Repository: jqlang/jq

Problem statement:
[Docs] Clarify the behvior of first, last, and nth on empty generators
I was confused by this behavior so thought it might be good to document it.


Hints:
Maybe you want to update the docs for `last/1` but editing the wrong place. `jq "[range(.)] | [[first], [last], [nth(5)]]" <<< "0"` yields `[[null],[null],[null]]`.
@itchyny I moved the doc update to the correct section
My understanding is that PR currently adds the text: 
```
`last` returns `null` instead.
```

Of course, that is true of the current implementation, but if this behavior is to be documented in the manual, there should be a disclaimer to the effect that the behavior may change in the future.

(As @itchyny recently pointed out:
> See https://github.com/jqlang/jq/pull/1869. This is a bug that should be fixed in the future.
)

`first/0` having the same behavior as `.[0]` seems logical to me.

The existence of `first`/`last`/`nth` seems a bit pointless anyways since you can just use `.[0]`, `.[-1]`, `.[n]` which are shorter. But that's an entirely seperate point.

Patch:
diff --git a/docs/content/manual/dev/manual.yml b/docs/content/manual/dev/manual.yml
index 60debde5e5..6cdff2ae71 100644
--- a/docs/content/manual/dev/manual.yml
+++ b/docs/content/manual/dev/manual.yml
@@ -3067,9 +3067,12 @@ sections:
           Note that `nth(n; expr)` doesn't support negative values of `n`.
 
         examples:
-          - program: '[first(range(.)), last(range(.)), nth(./2; range(.))]'
+          - program: '[first(range(.)), last(range(.)), nth(5; range(.))]'
             input: '10'
             output: ['[0,9,5]']
+          - program: '[first(empty), last(empty), nth(5; empty)]'
+            input: 'null'
+            output: ['[]']
 
       - title: "`first`, `last`, `nth(n)`"
         body: |
diff --git a/jq.1.prebuilt b/jq.1.prebuilt
index d3e44cfbd1..7ddc5c8ad0 100644
--- a/jq.1.prebuilt
+++ b/jq.1.prebuilt
@@ -3475,9 +3475,13 @@ The \fBnth(n; expr)\fR function extracts the nth value output by \fBexpr\fR\. No
 .
 .nf
 
-jq \'[first(range(\.)), last(range(\.)), nth(\./2; range(\.))]\'
+jq \'[first(range(\.)), last(range(\.)), nth(5; range(\.))]\'
    10
 => [0,9,5]
+
+jq \'[first(empty), last(empty), nth(5; empty)]\'
+   null
+=> []
 .
 .fi
 .
diff --git a/src/builtin.c b/src/builtin.c
index da4a770fac..dbd72f3de1 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -1879,6 +1879,33 @@ BINOPS
 #undef LIBM_DD
 #undef LIBM_DA
 
+// This is a hack to make last(g) yield no output values,
+// if g yields no output values, without using boxing.
+static block gen_last_1() {
+  block last_var = gen_op_var_fresh(STOREV, "last");
+  block is_empty_var = gen_op_var_fresh(STOREV, "is_empty");
+  block init = BLOCK(gen_op_simple(DUP),
+                     gen_const(jv_null()),
+                     last_var,
+                     gen_op_simple(DUP),
+                     gen_const(jv_true()),
+                     is_empty_var);
+  block call_arg = BLOCK(gen_call("arg", gen_noop()),
+                         gen_op_simple(DUP),
+                         gen_op_bound(STOREV, last_var),
+                         gen_const(jv_false()),
+                         gen_op_bound(STOREV, is_empty_var),
+                         gen_op_simple(BACKTRACK));
+  block if_empty = gen_op_simple(BACKTRACK);
+  return BLOCK(init,
+               gen_op_target(FORK, call_arg),
+               call_arg,
+               BLOCK(gen_op_bound(LOADVN, is_empty_var),
+                     gen_op_target(JUMP_F, if_empty),
+                     if_empty,
+                     gen_op_bound(LOADVN, last_var)));
+}
+
 struct bytecoded_builtin { const char* name; block code; };
 static block bind_bytecoded_builtins(block b) {
   block builtins = gen_noop();
@@ -1898,6 +1925,7 @@ static block bind_bytecoded_builtins(block b) {
       {"path", BLOCK(gen_op_simple(PATH_BEGIN),
                      gen_call("arg", gen_noop()),
                      gen_op_simple(PATH_END))},
+      {"last", gen_last_1()},
     };
     for (unsigned i=0; i<sizeof(builtin_def_1arg)/sizeof(builtin_def_1arg[0]); i++) {
       builtins = BLOCK(builtins, gen_function(builtin_def_1arg[i].name,
diff --git a/src/builtin.jq b/src/builtin.jq
index 0d1d6774ab..0e12bd5389 100644
--- a/src/builtin.jq
+++ b/src/builtin.jq
@@ -170,7 +170,6 @@ def all(condition): all(.[]; condition);
 def any(condition): any(.[]; condition);
 def all: all(.[]; .);
 def any: any(.[]; .);
-def last(g): reduce g as $item (null; $item);
 def nth($n; g):
   if $n < 0 then error("nth doesn't support negative indices")
   else first(skip($n; g)) end;


Test patch:
diff --git a/tests/jq.test b/tests/jq.test
index 404994e268..5c1b81d3ff 100644
--- a/tests/jq.test
+++ b/tests/jq.test
@@ -362,6 +362,10 @@ null
 10
 [0,9]
 
+[first(range(.)), last(range(.))]
+0
+[]
+
 [nth(0,5,9,10,15; range(.)), try nth(-1; range(.)) catch .]
 10
 [0,5,9,"nth doesn't support negative indices"]
diff --git a/tests/man.test b/tests/man.test
index 5529e011ef..5489c5e20e 100644
--- a/tests/man.test
+++ b/tests/man.test
@@ -881,10 +881,14 @@ false
 [0,1,2,3,4,5,6,7,8,9]
 [3,4,5,6,7,8,9]
 
-[first(range(.)), last(range(.)), nth(./2; range(.))]
+[first(range(.)), last(range(.)), nth(5; range(.))]
 10
 [0,9,5]
 
+[first(empty), last(empty), nth(5; empty)]
+null
+[]
+
 [range(.)]|[first, last, nth(5)]
 10
 [0,9,5]