Repository: tweag/nickel
Pull request #299 (2021-02-19 11:43:36)

Piecewise record definition using attribute paths
Depend on #292. Address #84. This PRs aims at making record definitions more pleasant. It is based on #84 and the early feedbacks of @garbas  and @Infinisil. It adds the following features:

## Field definition using attribute paths
Nested records can now be defined using a flat path 
```
{
   option.packages.stuff.enabled = true;
   other.foo.bar = 1;
}
// equivalent to
{
  option = { packages = { stuff = {enabled = true}}};
  other = {foo = { bar = 1}};
}
```
Type and metadata annotations are pushed to the leaf, that is
`{ foo.bar | default = 1}` is equivalent to `{foo = {bar | default = 1}}`.


## Field name escaping
To define a field which name is not a valid identifier, one currently has to resort to the interpolation syntax: `{ $"some-id.com" = stuff}`. The interpolation syntax supports arbitrary expressions, but the resulting expression must be a string anyway. The `$` is not used for interpolation anymore, so although it can be changed to `#`, it is out of sync. This PR changes it to a lighter syntax that just uses double quotes: `{"some-id.com" = stuff}`. Same for field access: `r."some-id.com"`. This syntax is common (supported in JSON, JavaScript, Nix, etc...). We can leverage stand strings to support interpolation out of the box, so that any previous dynamic interpolated expressions `{ $var = stuff}` can still be written as `{ "#{var}" = stuff}`, such that there's no new syntax.

## Piecewise definition
If the same field is defined multiple times, the different definitions are merged. This makes sense because merging is the natural semantics for combining multiple values in Nickel. Example:
```
{
  someoption.stuff.foo.bar.enabled = true;
  someoption.stuff.foo.baz.host = 2;
}
// equivalent to
{
  someoption.stuff.foo = {bar.enabled = true; baz.host = 2}
}
```

Merging then handles the cases of incompatible definitions:

```
nickel export <<< "{ a.b = 2; a.b = 3}"
error: Non mergeable terms
  ┌─ <stdin>:1:8
  │
1 │ {a.b = 2; a.b = 3}
  │        ^        ^ with this expression
  │        │         
  │        cannot merge this expression
```

## Tests
As tests are being reworked, this doesn't make sense to wast time to write them now just to rewrite them immediately after. Hence #84 stays open, with an item to remember to add some tests once the test suite has been upgraded.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e1b04939..a069e1d9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -91,9 +91,9 @@ dependencies = [
 
 [[package]]
 name = "bit-vec"
-version = "0.6.3"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+checksum = "5f0dc55f2d8a1a85650ac47858bb001b4c0dd73d79e3c455a842925e68d29cd3"
 
 [[package]]
 name = "bitflags"
@@ -110,16 +110,7 @@ dependencies = [
  "block-padding",
  "byte-tools",
  "byteorder",
- "generic-array 0.12.3",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
-dependencies = [
- "generic-array 0.14.4",
+ "generic-array",
 ]
 
 [[package]]
@@ -139,9 +130,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
 
 [[package]]
 name = "byteorder"
-version = "1.4.2"
+version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 
 [[package]]
 name = "cc"
@@ -204,25 +195,13 @@ dependencies = [
  "unicode-width",
 ]
 
-[[package]]
-name = "const_fn"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
-
-[[package]]
-name = "cpuid-bool"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
-
 [[package]]
 name = "crossbeam"
-version = "0.8.0"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd01a6eb3daaafa260f6fc94c3a6c36390abc2080e38e3e34ced87393fb77d80"
+checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e"
 dependencies = [
- "cfg-if 1.0.0",
+ "cfg-if 0.1.10",
  "crossbeam-channel",
  "crossbeam-deque",
  "crossbeam-epoch",
@@ -232,57 +211,59 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.0"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
 dependencies = [
- "cfg-if 1.0.0",
  "crossbeam-utils",
+ "maybe-uninit",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.0"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
+checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
 dependencies = [
- "cfg-if 1.0.0",
  "crossbeam-epoch",
  "crossbeam-utils",
+ "maybe-uninit",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
+checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
 dependencies = [
- "cfg-if 1.0.0",
- "const_fn",
+ "autocfg 1.0.1",
+ "cfg-if 0.1.10",
  "crossbeam-utils",
  "lazy_static",
+ "maybe-uninit",
  "memoffset",
  "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.1"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
 dependencies = [
- "cfg-if 1.0.0",
+ "cfg-if 0.1.10",
  "crossbeam-utils",
+ "maybe-uninit",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
 dependencies = [
  "autocfg 1.0.1",
- "cfg-if 1.0.0",
+ "cfg-if 0.1.10",
  "lazy_static",
 ]
 
@@ -329,16 +310,7 @@ version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
 dependencies = [
- "generic-array 0.12.3",
-]
-
-[[package]]
-name = "digest"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
-dependencies = [
- "generic-array 0.14.4",
+ "generic-array",
 ]
 
 [[package]]
@@ -369,7 +341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f525a586d310c87df72ebcd98009e57f1cc030c8c268305287a476beb653969"
 dependencies = [
  "lazy_static",
- "regex 1.4.3",
+ "regex 1.4.2",
  "serde",
  "strsim 0.9.3",
 ]
@@ -438,16 +410,6 @@ dependencies = [
  "typenum",
 ]
 
-[[package]]
-name = "generic-array"
-version = "0.14.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
-dependencies = [
- "typenum",
- "version_check",
-]
-
 [[package]]
 name = "getrandom"
 version = "0.2.2"
@@ -461,18 +423,18 @@ dependencies = [
 
 [[package]]
 name = "heck"
-version = "0.3.2"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
+checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
 dependencies = [
  "unicode-segmentation",
 ]
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.18"
+version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
 dependencies = [
  "libc",
 ]
@@ -488,9 +450,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "0.4.7"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
 
 [[package]]
 name = "kernel32-sys"
@@ -517,11 +479,11 @@ dependencies = [
  "itertools",
  "lalrpop-util",
  "petgraph",
- "regex 1.4.3",
- "regex-syntax 0.6.22",
+ "regex 1.4.2",
+ "regex-syntax 0.6.21",
  "serde",
  "serde_derive",
- "sha2 0.8.2",
+ "sha2",
  "string_cache",
  "term",
  "unicode-xid 0.1.0",
@@ -541,9 +503,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.85"
+version = "0.2.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"
+checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
 
 [[package]]
 name = "linked-hash-map"
@@ -562,11 +524,11 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.14"
+version = "0.4.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
 dependencies = [
- "cfg-if 1.0.0",
+ "cfg-if 0.1.10",
 ]
 
 [[package]]
@@ -588,21 +550,16 @@ dependencies = [
  "fnv",
  "proc-macro2",
  "quote",
- "regex-syntax 0.6.22",
+ "regex-syntax 0.6.21",
  "syn",
  "utf8-ranges",
 ]
 
 [[package]]
-name = "md-5"
-version = "0.9.1"
+name = "maybe-uninit"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
-dependencies = [
- "block-buffer 0.9.0",
- "digest 0.9.0",
- "opaque-debug 0.3.0",
-]
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
 
 [[package]]
 name = "memchr"
@@ -612,27 +569,27 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
 
 [[package]]
 name = "memoffset"
-version = "0.6.1"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
+checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
 dependencies = [
  "autocfg 1.0.1",
 ]
 
 [[package]]
 name = "minimad"
-version = "0.6.9"
+version = "0.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb5ed7ea8b54916318ac7ec6ff06b2b428fdf9cb54d1867714f699dbd1659ad4"
+checksum = "24fe6f533320d060be6644ff3967df0ddb2e3061164ab4976c9157995702e887"
 dependencies = [
  "lazy_static",
 ]
 
 [[package]]
 name = "mio"
-version = "0.7.7"
+version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7"
+checksum = "f33bc887064ef1fd66020c9adfc45bb9f33d75a42096c81e7c56c65b75dd1a8b"
 dependencies = [
  "libc",
  "log",
@@ -665,11 +622,9 @@ dependencies = [
  "assert_matches",
  "codespan",
  "codespan-reporting",
- "either",
  "lalrpop",
  "lalrpop-util",
  "logos",
- "md-5",
  "minimad",
  "pretty_assertions",
  "regex 0.2.11",
@@ -678,8 +633,6 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_yaml",
- "sha-1",
- "sha2 0.9.3",
  "simple-counter",
  "structopt",
  "termimad",
@@ -708,24 +661,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
-[[package]]
-name = "once_cell"
-version = "1.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
-
 [[package]]
 name = "opaque-debug"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
 
-[[package]]
-name = "opaque-debug"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
-
 [[package]]
 name = "ordermap"
 version = "0.3.5"
@@ -836,9 +777,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.8"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
 dependencies = [
  "proc-macro2",
 ]
@@ -998,14 +939,14 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.4.3"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
+checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
 dependencies = [
  "aho-corasick 0.7.15",
  "memchr",
- "regex-syntax 0.6.22",
- "thread_local 1.1.2",
+ "regex-syntax 0.6.21",
+ "thread_local 1.0.1",
 ]
 
 [[package]]
@@ -1019,9 +960,9 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.22"
+version = "0.6.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
+checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
 
 [[package]]
 name = "rustyline"
@@ -1068,18 +1009,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "serde"
-version = "1.0.123"
+version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
+checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.123"
+version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
+checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1088,9 +1029,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.61"
+version = "1.0.59"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
+checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
 dependencies = [
  "itoa",
  "ryu",
@@ -1099,9 +1040,9 @@ dependencies = [
 
 [[package]]
 name = "serde_yaml"
-version = "0.8.16"
+version = "0.8.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdd2af560da3c1fdc02cb80965289254fc35dff869810061e2d8290ee48848ae"
+checksum = "971be8f6e4d4a47163b405a3df70d14359186f9ab0f3a3ec37df144ca1ce089f"
 dependencies = [
  "dtoa",
  "linked-hash-map",
@@ -1109,49 +1050,23 @@ dependencies = [
  "yaml-rust",
 ]
 
-[[package]]
-name = "sha-1"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4b312c3731e3fe78a185e6b9b911a7aa715b8e31cce117975219aab2acf285d"
-dependencies = [
- "block-buffer 0.9.0",
- "cfg-if 1.0.0",
- "cpuid-bool",
- "digest 0.9.0",
- "opaque-debug 0.3.0",
-]
-
 [[package]]
 name = "sha2"
 version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
 dependencies = [
- "block-buffer 0.7.3",
- "digest 0.8.1",
+ "block-buffer",
+ "digest",
  "fake-simd",
- "opaque-debug 0.2.3",
-]
-
-[[package]]
-name = "sha2"
-version = "0.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
-dependencies = [
- "block-buffer 0.9.0",
- "cfg-if 1.0.0",
- "cpuid-bool",
- "digest 0.9.0",
- "opaque-debug 0.3.0",
+ "opaque-debug",
 ]
 
 [[package]]
 name = "signal-hook"
-version = "0.1.17"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
+checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed"
 dependencies = [
  "libc",
  "mio",
@@ -1160,9 +1075,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.3.0"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
+checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab"
 dependencies = [
  "libc",
 ]
@@ -1181,18 +1096,19 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
 
 [[package]]
 name = "smallvec"
-version = "1.6.1"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85"
 
 [[package]]
 name = "socket2"
-version = "0.3.19"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
+checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
+ "redox_syscall 0.1.57",
  "winapi 0.3.9",
 ]
 
@@ -1244,9 +1160,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
 
 [[package]]
 name = "structopt"
-version = "0.3.21"
+version = "0.3.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
+checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8"
 dependencies = [
  "clap",
  "lazy_static",
@@ -1255,9 +1171,9 @@ dependencies = [
 
 [[package]]
 name = "structopt-derive"
-version = "0.4.14"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
+checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8"
 dependencies = [
  "heck",
  "proc-macro-error",
@@ -1268,9 +1184,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.60"
+version = "1.0.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
+checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1289,18 +1205,18 @@ dependencies = [
 
 [[package]]
 name = "termcolor"
-version = "1.1.2"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
 dependencies = [
  "winapi-util",
 ]
 
 [[package]]
 name = "termimad"
-version = "0.9.5"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c0301aa5081d4c41d5ebf728ec7c3e825cebd1d8f301d95d65e27a9d93fef18"
+checksum = "fe9709f7deb2582e81b8bffd71ddc33ca46857c30fee361bcf6c0fd63caf8146"
 dependencies = [
  "crossbeam",
  "crossterm",
@@ -1321,18 +1237,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.23"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
+checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.23"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
+checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1350,11 +1266,11 @@ dependencies = [
 
 [[package]]
 name = "thread_local"
-version = "1.1.2"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
 dependencies = [
- "once_cell",
+ "lazy_static",
 ]
 
 [[package]]
@@ -1380,9 +1296,9 @@ checksum = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236"
 
 [[package]]
 name = "unicode-segmentation"
-version = "1.7.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
+checksum = "db8716a166f290ff49dabc18b44aa407cb7c6dbe1aa0971b44b8a24b0ca35aae"
 
 [[package]]
 name = "unicode-width"
@@ -1434,9 +1350,9 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
 
 [[package]]
 name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+version = "0.10.1+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9"
 
 [[package]]
 name = "winapi"
diff --git a/Cargo.toml b/Cargo.toml
index 05db0d9a..70343020 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,6 @@ lalrpop = "0.16.2"
 [dependencies]
 lalrpop-util = "0.16.2"
 regex = "0.2.1"
-either = "1.5.3"
 simple-counter = "0.1.0"
 codespan = "0.9.5"
 codespan-reporting = "0.9.5"
@@ -30,9 +29,6 @@ serde_yaml = "0.8.15"
 toml = "0.5.8"
 structopt = "0.3"
 void = "1"
-sha-1 = "0.9.3"
-sha2 = "0.9.3"
-md-5 = "0.9.1"
 
 termimad = { version = "0.9.1", optional = true }
 # Use the same version as termimad
diff --git a/flake.nix b/flake.nix
index 8dad490e..1c92f106 100644
--- a/flake.nix
+++ b/flake.nix
@@ -68,11 +68,7 @@
 
           doCheck = true;
 
-          checkPhase =
-            ''
-              cargo test --release --frozen --offline
-              cargo fmt --all -- --check
-            '';
+          checkPhase = "cargo test --release --frozen --offline";
 
           installPhase =
             ''
diff --git a/src/error.rs b/src/error.rs
index cb52a305..396e1ce1 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -71,14 +71,6 @@ pub enum EvalError {
     UnboundIdentifier(Ident, Option<RawSpan>),
     /// A thunk was entered during its own update.
     InfiniteRecursion(CallStack, Option<RawSpan>),
-    /// A serialization error occurred during a call to the builtin `serialize`.
-    SerializationError(SerializationError),
-    /// A parse error occurred during a call to the builtin `deserialize`.
-    DeserializationError(
-        String,          /* format */
-        String,          /* error message */
-        Option<RawSpan>, /* position of the call to deserialize */
-    ),
     /// An unexpected internal error.
     InternalError(String, Option<RawSpan>),
     /// Errors occurring rarely enough to not deserve a dedicated variant.
@@ -298,12 +290,6 @@ impl From<std::io::Error> for IOError {
     }
 }
 
-impl From<SerializationError> for EvalError {
-    fn from(error: SerializationError) -> EvalError {
-        EvalError::SerializationError(error)
-    }
-}
-
 /// Return an escaped version of a string. Used to sanitize strings before inclusion in error
 /// messages, which can contain ASCII code sequences, and in particular ANSI escape codes, that
 /// could alter Nickel's error messages.
@@ -364,25 +350,18 @@ impl ParseError {
     ) -> Self {
         use codespan::ByteOffset;
 
-        // error.line() should start at `1` according to the documentation, but in practice, it may
-        // be 0 for the error `json parse error: data did not match any variant of untagged enum
-        // Term`. Although this error should not happen, if it does, it's better to get a message
-        // than a panic message `subtract with overflow`.
-        let line_span = if error.line() == 0 {
-            None
-        } else {
-            files.line_span(file_id, (error.line() - 1) as u32).ok()
-        };
-
+        let line_span = files.line_span(file_id, (error.line() - 1) as u32);
         let start = line_span.map(|ls| ls.start() + ByteOffset::from(error.column() as i64 - 1));
         ParseError::ExternalFormatError(
             String::from("json"),
             error.to_string(),
-            start.map(|start| RawSpan {
-                src_id: file_id,
-                start,
-                end: start + ByteOffset::from(1),
-            }),
+            start
+                .map(|start| RawSpan {
+                    src_id: file_id,
+                    start,
+                    end: start + ByteOffset::from(1),
+                })
+                .ok(),
         )
     }
 
@@ -1033,18 +1012,6 @@ impl ToDiagnostic<FileId> for EvalError {
                     .with_labels(labels)
                     .with_notes(vec![String::from(INTERNAL_ERROR_MSG)])]
             }
-            EvalError::SerializationError(err) => err.to_diagnostic(files, contract_id),
-            EvalError::DeserializationError(format, msg, span_opt) => {
-                let labels = span_opt
-                    .as_ref()
-                    .map(|span| vec![primary(span).with_message("here")])
-                    .unwrap_or_default();
-
-                vec![Diagnostic::error()
-                    .with_message(format!("{} parse error: {}", format, msg))
-                    .with_labels(labels)
-                    .with_notes(vec![String::from(INTERNAL_ERROR_MSG)])]
-            }
         }
     }
 }
diff --git a/src/eval.rs b/src/eval.rs
index af054054..2c231248 100644
--- a/src/eval.rs
+++ b/src/eval.rs
@@ -590,29 +590,28 @@ where
             Term::RecRecord(ts) => {
                 // Thanks to the share normal form transformation, the content is either a constant or a
                 // variable.
-                let rec_env = ts.iter().try_fold::<_, _, Result<Environment, EvalError>>(
-                    HashMap::new(),
-                    |mut rec_env, (id, rt)| match rt.as_ref() {
-                        Term::Var(ref var_id) => {
-                            let thunk = env.get(var_id).ok_or_else(|| {
-                                EvalError::UnboundIdentifier(var_id.clone(), rt.pos.clone())
-                            })?;
-                            rec_env.insert(id.clone(), thunk.clone());
-                            Ok(rec_env)
-                        }
-                        _ => {
-                            // If we are in this branch, the term must be a constant after the
-                            // share normal form transformation, hence it should not need an
-                            // environment, which is why it is dropped.
-                            let closure = Closure {
-                                body: rt.clone(),
-                                env: HashMap::new(),
-                            };
-                            rec_env.insert(id.clone(), Thunk::new(closure, IdentKind::Let()));
-                            Ok(rec_env)
-                        }
-                    },
-                )?;
+                let rec_env =
+                    ts.iter()
+                        .try_fold(HashMap::new(), |mut rec_env, (id, rt)| match rt.as_ref() {
+                            Term::Var(ref var_id) => {
+                                let thunk = env.get(var_id).ok_or_else(|| {
+                                    EvalError::UnboundIdentifier(var_id.clone(), rt.pos.clone())
+                                })?;
+                                rec_env.insert(id.clone(), thunk.clone());
+                                Ok(rec_env)
+                            }
+                            _ => {
+                                // If we are in this branch, the term must be a constant after the
+                                // share normal form transformation, hence it should not need an
+                                // environment, which is why it is dropped.
+                                let closure = Closure {
+                                    body: rt.clone(),
+                                    env: HashMap::new(),
+                                };
+                                rec_env.insert(id.clone(), Thunk::new(closure, IdentKind::Let()));
+                                Ok(rec_env)
+                            }
+                        })?;
 
                 let new_ts = ts.into_iter().map(|(id, rt)| {
                     let RichTerm { term, pos } = rt;
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 5728a6d0..93d897c0 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -6,12 +6,12 @@ use crate::mk_app;
 use crate::types::{Types, AbsType};
 use super::ExtendedTerm;
 use super::utils::{StringKind, mk_span, mk_label, strip_indent, SwitchCase,
-    strip_indent_doc};
+    FieldPathElem, strip_indent_doc, build_record, elaborate_field_path,
+    ChunkLiteralPart};
 use std::ffi::OsString;
 use super::lexer::{Token, NormalToken, StringToken, MultiStringToken, LexicalError};
 use std::collections::HashMap;
 use std::cmp::min;
-use either::*;
 use codespan::FileId;
 
 grammar<'input>(src_id: FileId, );
@@ -182,7 +182,7 @@ RecordOperand: RichTerm = {
 
 RecordOperationChain: RichTerm = {
     <t: SpTerm<RecordOperand>> "." <id: Ident> => mk_term::op1(UnaryOp::StaticAccess(id), t),
-    <t: SpTerm<RecordOperand>> ".$" <t_id: SpTerm<Atom>> => mk_term::op2(BinaryOp::DynAccess(), t_id, t),
+    <t: SpTerm<RecordOperand>> "." <t_id: StrChunks> => mk_term::op2(BinaryOp::DynAccess(), t_id, t),
     <t: SpTerm<RecordOperand>> "-$" <t_id: SpTerm<Atom>> => mk_term::op2(BinaryOp::DynRemove(), t_id, t),
     <r: SpTerm<RecordOperand>> "$[" <id: Term> "=" <t: Term> "]" =>
         mk_app!(mk_term::op2(BinaryOp::DynExtend(), id, r), t),
@@ -242,25 +242,9 @@ Atom: RichTerm = {
     <StrChunks>,
     Ident => RichTerm::from(Term::Var(<>)),
     "`" <Ident> => RichTerm::from(Term::Enum(<>)),
-    "{" <fields: (RecordField ";")*> <last: RecordField?> "}" => {
-        let mut static_map = HashMap::new();
-        let mut dynamic_fields = Vec::new();
-
-        fields
-            .into_iter()
-            .map(|x| x.0)
-            .chain(last.into_iter())
-            .for_each(|field| match field {
-                Left((id, t)) => { static_map.insert(id, t) ;}
-                Right(t) => dynamic_fields.push(t),
-            });
-
-        let static_rec = RichTerm::from(Term::RecRecord(static_map));
-
-        dynamic_fields.into_iter().fold(static_rec, |rec, field| {
-            let (id_t, t) = field;
-            mk_app!(mk_term::op2(BinaryOp::DynExtend(), id_t, rec), t)
-        })
+    "{" <fields: (<RecordField> ";")*> <last: RecordField?> "}" => {
+        let fields = fields.into_iter().chain(last.into_iter());
+        RichTerm::from(build_record(fields))
     },
     "[" <terms: (SpTerm<Atom> ",")*> <last: Term?> "]" => {
         let terms : Vec<RichTerm> = terms.into_iter()
@@ -270,8 +254,8 @@ Atom: RichTerm = {
     }
 };
 
-RecordField: Either<(Ident, RichTerm), (RichTerm, RichTerm)> = {
-    <id: Ident> <ann: TypeAnnot?> "=" <t: Term> => {
+RecordField: (FieldPathElem, RichTerm) = {
+    <path: FieldPath> <ann: TypeAnnot?> "=" <t: Term> => {
         let t = if let Some((l, ty, r)) = ann {
             let pos = t.pos.clone();
             RichTerm::new(Term::Promise(ty.clone(), mk_label(ty, src_id, l, r), t), pos)
@@ -280,29 +264,32 @@ RecordField: Either<(Ident, RichTerm), (RichTerm, RichTerm)> = {
             t
         };
 
-        Either::Left((id, t))
+        elaborate_field_path(path, t)
     },
-    <l: @L> <id: Ident> <meta: MetaAnnot> <r: @R> <t: ("=" <Term>)?> => {
+    <l: @L> <path: FieldPath> <meta: MetaAnnot> <r: @R> <t: ("=" <Term>)?> => {
         let mut meta = meta;
         let pos = t.as_ref()
             .map(|t| t.pos.clone())
             .unwrap_or(Some(mk_span(src_id, l, r)));
         meta.value = t;
-        Either::Left((id, RichTerm::new(Term::MetaValue(meta), pos)))
-    },
-    "$" <id: SpTerm<Atom>> <ann: TypeAnnot?> "=" <t: Term> => {
-        let t = if let Some((l, ty, r)) = ann {
-            let pos = t.pos.clone();
-            RichTerm::new(Term::Promise(ty.clone(), mk_label(ty, src_id, l, r), t), pos)
-        }
-        else {
-            t
-        };
-
-        Either::Right((id, t))
+        let t = RichTerm::new(Term::MetaValue(meta), pos);
+        elaborate_field_path(path, t)
     }
 }
 
+FieldPath: Vec<FieldPathElem> = {
+    <elems: (<FieldPathElem> ".")*> <last: FieldPathElem> => {
+        let mut elems = elems;
+        elems.push(last);
+        elems
+    }
+};
+
+FieldPathElem: FieldPathElem = {
+    <Ident> => FieldPathElem::Ident(<>),
+    <StrChunks> => FieldPathElem::Expr(<>),
+};
+
 Pattern: Ident = {
     Ident,
 };
@@ -356,8 +343,8 @@ ChunkLiteral : String =
     <parts: ChunkLiteralPart+> => {
         parts.into_iter().fold(String::new(), |mut acc, part| {
             match part {
-                Either::Left(s) => acc.push_str(s),
-                Either::Right(c) => acc.push(c),
+                ChunkLiteralPart::Str(s) => acc.push_str(s),
+                ChunkLiteralPart::Char(c) => acc.push(c),
             };
 
             acc
@@ -370,13 +357,13 @@ HashBrace = { "#{", "multstr #{" };
 
 Str: String = "\"" <s: ChunkLiteral> "\"" => s;
 
-ChunkLiteralPart: Either<&'input str, char> = {
-    "str literal" => Either::Left(<>),
-    "str #" => Either::Left(<>),
-    "multstr literal" => Either::Left(<>),
-    "false interpolation" => Either::Left(<>),
-    "false end" => Either::Left(<>),
-    "str esc char" => Either::Right(<>),
+ChunkLiteralPart: ChunkLiteralPart<'input> = {
+    "str literal" => ChunkLiteralPart::Str(<>),
+    "str #" => ChunkLiteralPart::Str(<>),
+    "multstr literal" => ChunkLiteralPart::Str(<>),
+    "false interpolation" => ChunkLiteralPart::Str(<>),
+    "false end" => ChunkLiteralPart::Str(<>),
+    "str esc char" => ChunkLiteralPart::Char(<>),
     };
 
 UOp: UnaryOp = {
@@ -517,9 +504,6 @@ BOpPre: BinaryOp = {
     "hasField" => BinaryOp::HasField(),
     "elemAt" => BinaryOp::ListElemAt(),
     "tag" => BinaryOp::Tag(),
-    "hash" => BinaryOp::Hash(),
-    "serialize" => BinaryOp::Serialize(),
-    "deserialize" => BinaryOp::Deserialize(),
 }
 
 Types: Types = {
@@ -631,7 +615,6 @@ extern {
         ";" => Token::Normal(NormalToken::SemiCol),
         "&" => Token::Normal(NormalToken::Ampersand),
         "." => Token::Normal(NormalToken::Dot),
-        ".$" => Token::Normal(NormalToken::DotDollar),
         "$[" => Token::Normal(NormalToken::DollarBracket),
         "#{" => Token::Str(StringToken::HashBrace),
         "multstr #{" => Token::MultiStr(MultiStringToken::Interpolation),
@@ -649,7 +632,6 @@ extern {
         "||" => Token::Normal(NormalToken::DoublePipe),
         "!" => Token::Normal(NormalToken::Bang),
 
-        "$=" => Token::Normal(NormalToken::DollarEquals),
         "fun" => Token::Normal(NormalToken::Fun),
         "import" => Token::Normal(NormalToken::Import),
         "|" => Token::Normal(NormalToken::Pipe),
@@ -706,10 +688,6 @@ extern {
         "default" => Token::Normal(NormalToken::Default),
         "doc" => Token::Normal(NormalToken::Doc),
 
-        "hash" => Token::Normal(NormalToken::OpHash),
-        "serialize" => Token::Normal(NormalToken::Serialize),
-        "deserialize" => Token::Normal(NormalToken::Deserialize),
-
         "{" => Token::Normal(NormalToken::LBrace),
         "}" => Token::Normal(NormalToken::RBrace),
         "[" => Token::Normal(NormalToken::LBracket),
diff --git a/src/main.rs b/src/main.rs
index 91962cdc..a524feb5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,8 +30,6 @@ use std::{fmt, fs, io, process};
 // use std::ffi::OsStr;
 use structopt::StructOpt;
 
-extern crate either;
-
 /// Command-line options and subcommands.
 #[derive(StructOpt, Debug)]
 /// The interpreter of the Nickel language.
@@ -191,10 +189,11 @@ fn export(
                 .map_err(|err| SerializationError::Other(err.to_string())),
             ExportFormat::Yaml => serde_yaml::to_writer(file, &rt)
                 .map_err(|err| SerializationError::Other(err.to_string())),
-            ExportFormat::Toml => toml::Value::try_from(&rt)
+            ExportFormat::Toml => toml::ser::to_string_pretty(&rt)
                 .map_err(|err| SerializationError::Other(err.to_string()))
-                .and_then(|v| {
-                    write!(file, "{}", v).map_err(|err| SerializationError::Other(err.to_string()))
+                .and_then(|s| {
+                    file.write_all(s.as_bytes())
+                        .map_err(|err| SerializationError::Other(err.to_string()))
                 }),
             ExportFormat::Raw => match *rt.term {
                 Term::Str(s) => file
diff --git a/src/operation.rs b/src/operation.rs
index f63eda04..90767ea2 100644
--- a/src/operation.rs
+++ b/src/operation.rs
@@ -6,8 +6,9 @@
 //! the functions [`process_unary_operation`](fn.process_unary_operation.html) and
 //! [`process_binary_operation`](fn.process_binary_operation.html) receive evaluated operands and
 //! implement the actual semantics of operators.
-use crate::error::{EvalError, SerializationError};
-use crate::eval::{subst, CallStack, Closure, Environment};
+use crate::error::EvalError;
+use crate::eval::Environment;
+use crate::eval::{CallStack, Closure};
 use crate::identifier::Ident;
 use crate::label::ty_path;
 use crate::merge;
@@ -18,8 +19,6 @@ use crate::term::make as mk_term;
 use crate::term::{BinaryOp, RichTerm, StrChunk, Term, UnaryOp};
 use crate::transformations::Closurizable;
 use crate::{mk_app, mk_fun};
-use crate::{serialize, serialize::ExportFormat};
-use md5::digest::Digest;
 use simple_counter::*;
 use std::collections::HashMap;
 use std::iter::Extend;
@@ -1316,167 +1315,6 @@ fn process_binary_operation(
             env2,
             pos_op,
         ),
-        BinaryOp::Hash() => {
-            let mk_err_fst = |t1| {
-                Err(EvalError::TypeError(
-                    String::from("Enum <Md5, Sha1, Sha256, Sha512>"),
-                    String::from("hash, 1st argument"),
-                    fst_pos,
-                    RichTerm {
-                        term: t1,
-                        pos: pos1,
-                    },
-                ))
-            };
-
-            if let Term::Enum(ref id) = t1.as_ref() {
-                if let Term::Str(s) = *t2 {
-                    let result = match id.to_string().as_str() {
-                        "Md5" => {
-                            let mut hasher = md5::Md5::new();
-                            hasher.update(s);
-                            format!("{:x}", hasher.finalize())
-                        }
-                        "Sha1" => {
-                            let mut hasher = sha1::Sha1::new();
-                            hasher.update(s);
-                            format!("{:x}", hasher.finalize())
-                        }
-                        "Sha256" => {
-                            let mut hasher = sha2::Sha256::new();
-                            hasher.update(s);
-                            format!("{:x}", hasher.finalize())
-                        }
-                        "Sha512" => {
-                            let mut hasher = sha2::Sha512::new();
-                            hasher.update(s);
-                            format!("{:x}", hasher.finalize())
-                        }
-                        _ => return mk_err_fst(t1),
-                    };
-
-                    Ok(Closure::atomic_closure(Term::Str(result).into()))
-                } else {
-                    Err(EvalError::TypeError(
-                        String::from("Str"),
-                        String::from("hash, 2nd argument"),
-                        snd_pos,
-                        RichTerm {
-                            term: t2,
-                            pos: pos2,
-                        },
-                    ))
-                }
-            } else {
-                mk_err_fst(t1)
-            }
-        }
-        BinaryOp::Serialize() => {
-            let mk_err_fst = |t1| {
-                Err(EvalError::TypeError(
-                    String::from("Enum <Json, Yaml, Toml>"),
-                    String::from("serialize, 1st argument"),
-                    fst_pos,
-                    RichTerm {
-                        term: t1,
-                        pos: pos1,
-                    },
-                ))
-            };
-
-            if let Term::Enum(ref id) = t1.as_ref() {
-                // Serialization needs all variables term to be fully substituted
-                let global_env = Environment::new();
-                let rt2 = subst(
-                    RichTerm {
-                        term: t2,
-                        pos: pos2,
-                    },
-                    &global_env,
-                    &env2,
-                );
-
-                let result = match id.to_string().as_str() {
-                    "Json" => {
-                        serialize::validate(&rt2, ExportFormat::Json)?;
-                        serde_json::to_string_pretty(&rt2)
-                            .map_err(|err| SerializationError::Other(err.to_string()))?
-                    }
-                    "Yaml" => {
-                        serialize::validate(&rt2, ExportFormat::Yaml)?;
-                        serde_yaml::to_string(&rt2)
-                            .map_err(|err| SerializationError::Other(err.to_string()))?
-                    }
-                    "Toml" => {
-                        serialize::validate(&rt2, ExportFormat::Toml)?;
-                        toml::Value::try_from(&rt2)
-                            .map(|v| format!("{}", v))
-                            .map_err(|err| SerializationError::Other(err.to_string()))?
-                    }
-                    _ => return mk_err_fst(t1),
-                };
-
-                Ok(Closure::atomic_closure(Term::Str(result).into()))
-            } else {
-                mk_err_fst(t1)
-            }
-        }
-        BinaryOp::Deserialize() => {
-            let mk_err_fst = |t1| {
-                Err(EvalError::TypeError(
-                    String::from("Enum <Json, Yaml, Toml>"),
-                    String::from("deserialize, 1st argument"),
-                    fst_pos,
-                    RichTerm {
-                        term: t1,
-                        pos: pos1,
-                    },
-                ))
-            };
-
-            if let Term::Enum(ref id) = t1.as_ref() {
-                if let Term::Str(s) = *t2 {
-                    let rt: RichTerm = match id.to_string().as_str() {
-                        "Json" => serde_json::from_str(&s).map_err(|err| {
-                            EvalError::DeserializationError(
-                                String::from("json"),
-                                format!("{}", err),
-                                pos_op,
-                            )
-                        })?,
-                        "Yaml" => serde_yaml::from_str(&s).map_err(|err| {
-                            EvalError::DeserializationError(
-                                String::from("yaml"),
-                                format!("{}", err),
-                                pos_op,
-                            )
-                        })?,
-                        "Toml" => toml::from_str(&s).map_err(|err| {
-                            EvalError::DeserializationError(
-                                String::from("toml"),
-                                format!("{}", err),
-                                pos_op,
-                            )
-                        })?,
-                        _ => return mk_err_fst(t1),
-                    };
-
-                    Ok(Closure::atomic_closure(rt))
-                } else {
-                    Err(EvalError::TypeError(
-                        String::from("Str"),
-                        String::from("deserialize, 2nd argument"),
-                        snd_pos,
-                        RichTerm {
-                            term: t2,
-                            pos: pos2,
-                        },
-                    ))
-                }
-            } else {
-                mk_err_fst(t1)
-            }
-        }
     }
 }
 
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index fd83a161..6f2afbe6 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -90,12 +90,8 @@ pub enum NormalToken<'input> {
     Ampersand,
     #[token(".")]
     Dot,
-    #[token(".$")]
-    DotDollar,
     #[token("$[")]
     DollarBracket,
-    #[token("$=")]
-    DollarEquals,
     #[token("\"")]
     DoubleQuote,
     #[token("-$")]
@@ -220,12 +216,6 @@ pub enum NormalToken<'input> {
     #[token("doc")]
     Doc,
 
-    #[token("%hash%")]
-    OpHash,
-    #[token("%serialize%")]
-    Serialize,
-    #[token("%deserialize%")]
-    Deserialize,
     #[token("{")]
     LBrace,
     #[token("}")]
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index ea29d3ad..c1be5410 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -1,10 +1,13 @@
 use crate::identifier::Ident;
 /// A few helpers to generate position spans and labels easily during parsing
 use crate::label::Label;
+use crate::mk_app;
 use crate::position::RawSpan;
-use crate::term::{RichTerm, StrChunk};
+use crate::term::{make as mk_term, BinaryOp, RichTerm, StrChunk, Term};
 use crate::types::Types;
 use codespan::FileId;
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
 
 /// Distinguish between the standard string separators `"`/`"` and the multi-line string separators
 /// `m#"`/`"#m` in the parser.
@@ -21,6 +24,88 @@ pub enum SwitchCase {
     Default(RichTerm),
 }
 
+/// Left hand side of a record field declaration.
+#[derive(Clone, Debug)]
+pub enum FieldPathElem {
+    /// A static field declaration: `{ foo = .. }`
+    Ident(Ident),
+    /// A quoted field declaration: `{ "#{protocol}" = .. }`
+    ///
+    /// In practice, the argument must always be `StrChunks`, but since we also need to keep track
+    /// of the associated span it's handier to just use a `RichTerm`.
+    Expr(RichTerm),
+}
+
+/// A string chunk literal atom, being either a string or a single char.
+///
+/// Because of the way the lexer handles escaping and interpolation, a contiguous static string
+/// `"Some \\ \#{escaped} string"` will be lexed as a sequence of such atoms.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ChunkLiteralPart<'input> {
+    Str(&'input str),
+    Char(char),
+}
+
+/// Elaborate a record field definition specified as a path, like `a.b.c = foo`, into a regular
+/// flat definition `a = {b = {c = foo}}`.
+///
+/// # Preconditions
+/// - /!\ path must be **non-empty**, otherwise this function panics
+pub fn elaborate_field_path(
+    path: Vec<FieldPathElem>,
+    content: RichTerm,
+) -> (FieldPathElem, RichTerm) {
+    let mut it = path.into_iter();
+    let fst = it.next().unwrap();
+
+    let content = it.rev().fold(content, |acc, path_elem| match path_elem {
+        FieldPathElem::Ident(id) => {
+            let mut map = HashMap::new();
+            map.insert(id, acc);
+            Term::Record(map).into()
+        }
+        FieldPathElem::Expr(exp) => {
+            let empty = Term::Record(HashMap::new());
+            mk_app!(mk_term::op2(BinaryOp::DynExtend(), exp, empty), acc)
+        }
+    });
+
+    (fst, content)
+}
+
+/// Build a record from a list of field definitions. If a field is defined several times, the
+/// different definitions are merged.
+pub fn build_record<I>(fields: I) -> Term
+where
+    I: IntoIterator<Item = (FieldPathElem, RichTerm)>,
+{
+    let mut static_map = HashMap::new();
+    let mut dynamic_fields = Vec::new();
+
+    fields.into_iter().for_each(|field| match field {
+        (FieldPathElem::Ident(id), t) => {
+            match static_map.entry(id) {
+                Entry::Occupied(mut occpd) => {
+                    // temporary putting null in the entry to take the previous value.
+                    let prev = occpd.insert(Term::Null.into());
+                    occpd.insert(mk_term::op2(BinaryOp::Merge(), prev, t));
+                }
+                Entry::Vacant(vac) => {
+                    vac.insert(t);
+                }
+            }
+        }
+        (FieldPathElem::Expr(e), t) => dynamic_fields.push((e, t)),
+    });
+
+    dynamic_fields
+        .into_iter()
+        .fold(Term::RecRecord(static_map), |rec, field| {
+            let (id_t, t) = field;
+            Term::App(mk_term::op2(BinaryOp::DynExtend(), id_t, rec), t)
+        })
+}
+
 /// Make a span from parser byte offsets.
 pub fn mk_span(src_id: FileId, l: usize, r: usize) -> RawSpan {
     RawSpan {
diff --git a/src/program.rs b/src/program.rs
index 3a9d2286..e1f1cdd8 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -213,7 +213,6 @@ mod tests {
     use crate::parser::{grammar, lexer};
     use assert_matches::assert_matches;
     use codespan::Files;
-    use serde_json::json;
     use std::io::Cursor;
 
     fn parse(s: &str) -> Option<RichTerm> {
@@ -502,23 +501,23 @@ Assume(#alwaysTrue, false)
         );
 
         assert_eq!(
-            eval_string("({ $(if true then \"foo\" else \"bar\") = false; bar = true; }).foo"),
+            eval_string("({ \"#{if true then \"foo\" else \"bar\"}\" = false; bar = true; }).foo"),
             Ok(Term::Bool(false)),
         );
 
         assert_eq!(
-            eval_string("({ foo = 3; bar = true; }).$(\"bar\")"),
+            eval_string("({ foo = 3; bar = true; }).\"bar\""),
             Ok(Term::Bool(true)),
         );
 
         assert_eq!(
             eval_string(
-                "({ $(if true then \"foo\" else \"bar\") = false; bar = true; }).$(\"foo\")"
+                "({ \"#{if true then \"foo\" else \"bar\"}\" = false; bar = true; }).\"foo\""
             ),
             Ok(Term::Bool(false)),
         );
 
-        eval_string("({ $(if false then \"foo\" else \"bar\") = false; bar = true; }).foo")
+        eval_string("({ \"#{if false then \"foo\" else \"bar\"}\" = false; bar = true; }).foo")
             .unwrap_err();
     }
 
@@ -1535,75 +1534,4 @@ too
             Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
         );
     }
-
-    /// Check that `deserialize (serialize term) == term`.
-    macro_rules! assert_ser_inv {
-        ($t:expr) => {
-            assert_peq!(
-                $t,
-                format!(
-                    "%deserialize% `Json (%serialize% `Json (let x = {} in %deepSeq% x x))",
-                    $t
-                )
-            );
-            assert_peq!(
-                $t,
-                format!(
-                    "%deserialize% `Yaml (%serialize% `Yaml (let x = {} in %deepSeq% x x))",
-                    $t
-                )
-            );
-            assert_peq!(
-                $t,
-                format!(
-                    "%deserialize% `Toml (%serialize% `Toml (let x = {} in %deepSeq% x x))",
-                    $t
-                )
-            );
-        };
-    }
-
-    /// Check that `serialize (deserialize s) == s`.
-    macro_rules! assert_deser_inv {
-        ($data:expr) => {
-            let as_str = serde_json::to_string_pretty(&$data).unwrap();
-            assert_peq!(
-                format!("\"{}\"", as_str.replace("\"", "\\\"")),
-                format!(
-                    "%serialize% `Json (%deserialize% `Json m##\"{}\"##m)",
-                    as_str
-                )
-            );
-            let as_str = serde_yaml::to_string(&$data).unwrap();
-            assert_peq!(
-                format!("\"{}\"", as_str.replace("\"", "\\\"")),
-                format!(
-                    "%serialize% `Yaml (%deserialize% `Yaml m##\"{}\"##m)",
-                    as_str
-                )
-            );
-            let as_str = format!("{}", toml::Value::try_from(&$data).unwrap());
-            assert_peq!(
-                format!("\"{}\"", as_str.replace("\"", "\\\"")),
-                format!(
-                    "%serialize% `Toml (%deserialize% `Toml m##\"{}\"##m)",
-                    as_str
-                )
-            );
-        };
-    }
-
-    #[test]
-    fn serialize_builtin_laws() {
-        assert_ser_inv!("{val = 1 + 1}");
-        assert_ser_inv!("{val = \"Some string\"}");
-        assert_ser_inv!("{val = [\"a\", 3, []]}");
-        assert_ser_inv!("{a = {foo = {bar = \"2\"}}; b = false; c = [{d = \"e\"}, {d = \"f\"}]}");
-
-        assert_deser_inv!(json!({"a": 1.0, "b": 4.0, "c": 3.0}));
-        assert_deser_inv!(json!({"a": {"b": {"c": "richtig"}}}));
-        assert_deser_inv!(
-            json!({"foo": 1.0, "bar": ["str", true], "baz": {"subfoo": true, "subbar": 0.0}})
-        );
-    }
 }
diff --git a/src/term.rs b/src/term.rs
index 309cb71b..ff5652f6 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -616,12 +616,6 @@ pub enum BinaryOp {
     ListElemAt(),
     /// The merge operator (see the [merge module](../merge/index.html)).
     Merge(),
-    /// Hash a string.
-    Hash(),
-    /// Serialize a value to a string.
-    Serialize(),
-    /// Deserialize a string to a value.
-    Deserialize(),
 }
 
 impl BinaryOp {
diff --git a/src/typecheck.rs b/src/typecheck.rs
index d049a71c..0163c338 100644
--- a/src/typecheck.rs
+++ b/src/typecheck.rs
@@ -813,7 +813,7 @@ pub fn apparent_type(t: &Term, envs: Option<&Envs>) -> ApparentType {
         Term::Sym(_) => ApparentType::Inferred(Types(AbsType::Sym())),
         Term::Str(_) | Term::StrChunks(_) => ApparentType::Inferred(Types(AbsType::Str())),
         Term::List(_) => {
-            ApparentType::Approximated(Types(AbsType::List(Box::new(Types(AbsType::Dyn())))))
+            ApparentType::Inferred(Types(AbsType::List(Box::new(Types(AbsType::Dyn())))))
         }
         Term::Var(id) => envs
             .and_then(|envs| envs.get(id))
@@ -1679,33 +1679,6 @@ pub fn get_bop_type(state: &mut State, op: &BinaryOp) -> Result<TypeWrapper, Typ
         }
         // Dyn -> Dyn -> Dyn
         BinaryOp::Merge() => mk_tyw_arrow!(AbsType::Dyn(), AbsType::Dyn(), AbsType::Dyn()),
-        // <Md5, Sha1, Sha256, Sha512> -> Str -> Str
-        BinaryOp::Hash() => mk_tyw_arrow!(
-            mk_tyw_enum!(
-                "Md5",
-                "Sha1",
-                "Sha256",
-                "Sha512",
-                mk_typewrapper::row_empty()
-            ),
-            mk_typewrapper::str(),
-            mk_typewrapper::str()
-        ),
-        // forall a. <Json, Yaml, Toml> -> a -> Str
-        BinaryOp::Serialize() => {
-            let ty_input = TypeWrapper::Ptr(new_var(state.table));
-            mk_tyw_arrow!(
-                mk_tyw_enum!("Json", "Yaml", "Toml", mk_typewrapper::row_empty()),
-                ty_input,
-                mk_typewrapper::str()
-            )
-        }
-        // <Json, Yaml, Toml> -> Str -> Dyn
-        BinaryOp::Deserialize() => mk_tyw_arrow!(
-            mk_tyw_enum!("Json", "Yaml", "Toml", mk_typewrapper::row_empty()),
-            mk_typewrapper::str(),
-            mk_typewrapper::dynamic()
-        ),
     })
 }
 
@@ -2228,13 +2201,14 @@ mod tests {
 
     #[test]
     fn dynamic_record_simple() {
-        parse_and_typecheck("{ $(if true then \"foo\" else \"bar\") = 2; } : {_ : Num}").unwrap();
+        parse_and_typecheck("{ \"#{if true then \"foo\" else \"bar\"}\" = 2; } : {_ : Num}")
+            .unwrap();
 
-        parse_and_typecheck("({ $(if true then \"foo\" else \"bar\") = 2; }.$(\"bla\")) : Num")
+        parse_and_typecheck("({ \"#{if true then \"foo\" else \"bar\"}\" = 2; }.\"bla\") : Num")
             .unwrap();
 
         parse_and_typecheck(
-            "({ $(if true then \"foo\" else \"bar\") = 2; $(\"foo\") = true; }.$(\"bla\")) : Num",
+            "({ \"#{if true then \"foo\" else \"bar\"}\" = 2; \"foo\" = true; }.\"bla\") : Num",
         )
         .unwrap_err();
 
@@ -2415,13 +2389,4 @@ mod tests {
         parse_and_typecheck("let x = false in (x || true : Bool)").unwrap();
         parse_and_typecheck("let x = false in let y = x in let z = y in (z : Bool)").unwrap();
     }
-
-    /// Regression test following, see [#297](https://github.com/tweag/nickel/pull/297). Check that
-    /// [apparent_type](../fn.apparent_type.html) doesn't silently convert list literals from `List
-    /// T` (for `T` a type or a type variable) to `List Dyn`.
-    #[test]
-    fn apparent_type_lists() {
-        parse_and_typecheck("{foo = [1]} : {foo : List Num}").unwrap();
-        parse_and_typecheck("(let y = [] in y) : forall a. List a").unwrap();
-    }
 }
diff --git a/stdlib/builtins.ncl b/stdlib/builtins.ncl
index a7a1188c..2a2d6a3c 100644
--- a/stdlib/builtins.ncl
+++ b/stdlib/builtins.ncl
@@ -6,34 +6,8 @@
     isFun : Dyn -> Bool = fun x => %isFun% x;
     isList : Dyn -> Bool = fun x => %isList% x;
     isRecord : Dyn -> Bool = fun x => %isRecord% x;
-    typeOf : Dyn -> <
-      TypeNum,
-      TypeBool,
-      TypeStr,
-      TypeFun,
-      TypeList,
-      TypeRecord,
-      Other> = fun x =>
-      if %isNum% x then `TypeNum
-      else if %isBool% x then `TypeBool
-      else if %isStr% x then `TypeStr
-      else if %isFun% x then `TypeFun
-      else if %isList% x then `TypeList
-      else if %isRecord% x then `TypeRecord
-      else `Other;
 
     seq : forall a. Dyn -> a -> a = fun x y => %seq% x y;
     deepSeq : forall a. Dyn -> a -> a = fun x y => %deepSeq% x y;
-
-    id : forall a. a -> a = fun x => x;
-
-    hash : <Md5, Sha1, Sha256, Sha512> -> Str -> Str =
-      fun type s => %hash% type s;
-
-    serialize : <Json, Toml, Yaml> -> Dyn -> Str = fun format x =>
-      %serialize% format (%deepSeq% x x);
-
-    deserialize : <Json, Toml, Yaml> -> Str -> Dyn = fun format x =>
-      %deserialize% format x;
   }
 }
diff --git a/stdlib/contracts.ncl b/stdlib/contracts.ncl
index b1fc934a..68ec43e5 100644
--- a/stdlib/contracts.ncl
+++ b/stdlib/contracts.ncl
@@ -44,7 +44,7 @@
 
   record_extend = fun field contr cont acc l t =>
       if %hasField% field t then
-          let acc = acc$[field = contr (%goField% field l) (t.$field)] in
+          let acc = acc$[field = contr (%goField% field l) (t."#{field}")] in
           let t = t -$ field in
           cont acc l t
       else
@@ -57,7 +57,7 @@
               let rest = (t -$ magic_fld) in
               if rest == {} then
                   let fail = %blame% (%tag% "polymorphic tail mismatch" l) in
-                  let inner = %unwrap% sy (t.$magic_fld) fail in
+                  let inner = %unwrap% sy (t."#{magic_fld}") fail in
                   acc & inner
               else
                   %blame% (%tag% "extra field `#{%head% (%fieldsOf% rest)}`" l)


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index d8a8bce8..c4f8695e 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -177,11 +177,11 @@ fn record_terms() {
     );
 
     assert_eq!(
-        parse_without_pos("{ a = 1; $123 = (if 4 then 5 else 6); d = 42;}"),
+        parse_without_pos("{ a = 1; \"#{123}\" = (if 4 then 5 else 6); d = 42;}"),
         mk_app!(
             mk_term::op2(
                 BinaryOp::DynExtend(),
-                Num(123.),
+                StrChunks(vec![StrChunk::expr(RichTerm::from(Num(123.)))]),
                 RecRecord(
                     vec![
                         (Ident::from("a"), Num(1.).into()),