Repository: tweag/nickel (Rust)

Issue:
Field without definition nor contract are assigned the value `null`
**Describe the bug**
One can write field without definitions in Nickel, e.g. for contracts or to indicate that a field is not defined but is expected to be brought by a subsequent merge. Those fields are different from `null`, and are a dedicated construct.

However, if one doesn't specify any metavalue nor a definition, the field is given the value `null` instead of being undefined, which is wrong.

**To Reproduce**
```nickel
nickel>{foo | Num, bar = foo + 1}.foo
error: missing definition for `foo`
  ┌─ repl-input-3:1:1
  │
1 │ ({foo | Dyn, bar = foo + 1}).foo
  │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^----
  │ │
  │ accessed here
  │ in this record

nickel>{foo, bar = foo + 1}.foo
null
```

**Expected behavior**
A field without an `= exp` right-hand side should not have a definition, and this shouldn't depend on meta-annotations being present or not.



Discussion:
Related:

```
nickel> ({foo : Num} & {foo = 1}).foo
1

nickel> ({foo} & {foo = 1}).foo
error: non mergeable terms
  ┌─ repl-input-2:1:3
  │
1 │ ({foo} & {foo = 1}).foo
  │   ^^^           ^ with this expression
  │   │              
  │   cannot merge this expression
```

I'd expect that the result of `({foo} & {foo = 1}).foo` is 1.
Also,

As expected:
```sh-session
$ ./target/debug/nickel pprint-ast <<< '{foo : Num}.foo'
{ foo : Num  , }.foo
```

but:

```sh-session
$ ./target/debug/nickel pprint-ast <<< '{foo}.foo'
{ foo = null, }.foo
```

I would expect the result here to be `{ foo, }.foo`. So the problem may be rooted in the parser.



Patch:
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 1cccc84cfa..050c8da25b 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -276,7 +276,7 @@ where
 ///
 /// # Arguments
 ///
-/// - `t0`: the term to evaluate
+/// - `clos`: the closure to evaluate
 /// - `global_env`: the global environment containing the builtin functions of the language. Accessible from anywhere in the
 /// program.
 /// - `resolver`: the interface to fetch imports.
@@ -562,7 +562,7 @@ where
             Term::MetaValue(meta) if enriched_strict => {
                 if meta.value.is_some() {
                     /* Since we are forcing a metavalue, we are morally evaluating `force t` rather
-                     * than `t` iteself.  Updating a thunk after having performed this forcing may
+                     * than `t` itself. Updating a thunk after having performed this forcing may
                      * alter the semantics of the program in an unexpected way (see issue
                      * https://github.com/tweag/nickel/issues/123): we update potential thunks now
                      * so that their content remains a meta value.
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 1ffefed365..08c9424cad 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -337,7 +337,7 @@ RecordField: (FieldPath, RichTerm) = {
             if let Some(deft) = t {
                 deft
             } else {
-                RichTerm::new(Term::Null, pos)
+                RichTerm::new(Term::MetaValue(MetaValue::new()), pos)
             }
         };
 


Test patch:
diff --git a/tests/records_fail.rs b/tests/records_fail.rs
index b0e96c5d67..81b17cbb42 100644
--- a/tests/records_fail.rs
+++ b/tests/records_fail.rs
@@ -49,3 +49,19 @@ fn dynamic_not_recursive() {
         Err(Error::TypecheckError(TypecheckError::UnboundIdentifier(..)))
     );
 }
+
+#[test]
+fn missing_field() {
+    assert_matches!(
+        eval("{foo | Num, bar = foo + 1}.foo"),
+        Err(Error::EvalError(EvalError::MissingFieldDef(..)))
+    );
+    assert_matches!(
+        eval("{foo : Num, bar = foo + 1}.foo"),
+        Err(Error::EvalError(EvalError::MissingFieldDef(..)))
+    );
+    assert_matches!(
+        eval("{foo, bar = foo + 1}.foo"),
+        Err(Error::EvalError(EvalError::MissingFieldDef(..)))
+    )
+}