std/lzw: add "#truncated input" error

name                                         old speed      new speed      delta

wuffs_gif_decode_1k_bw/clang11                659MB/s ± 0%   748MB/s ± 0%  +13.43%  (p=0.008 n=5+5)
wuffs_gif_decode_1k_color_full_init/clang11   182MB/s ± 0%   178MB/s ± 0%   -2.17%  (p=0.008 n=5+5)
wuffs_gif_decode_1k_color_part_init/clang11   223MB/s ± 0%   219MB/s ± 0%   -2.00%  (p=0.008 n=5+5)
wuffs_gif_decode_10k_bgra/clang11             799MB/s ± 0%   795MB/s ± 0%   -0.52%  (p=0.008 n=5+5)
wuffs_gif_decode_10k_indexed/clang11          211MB/s ± 0%   210MB/s ± 0%   -0.47%  (p=0.008 n=5+5)
wuffs_gif_decode_20k/clang11                  250MB/s ± 1%   254MB/s ± 0%   +1.75%  (p=0.008 n=5+5)
wuffs_gif_decode_100k_artificial/clang11      581MB/s ± 0%   568MB/s ± 0%   -2.09%  (p=0.008 n=5+5)
wuffs_gif_decode_100k_realistic/clang11       221MB/s ± 0%   221MB/s ± 0%     ~     (p=0.690 n=5+5)
wuffs_gif_decode_1000k_full_init/clang11      222MB/s ± 0%   224MB/s ± 0%   +0.65%  (p=0.008 n=5+5)
wuffs_gif_decode_1000k_part_init/clang11      223MB/s ± 0%   224MB/s ± 0%   +0.58%  (p=0.008 n=5+5)
wuffs_gif_decode_anim_screencap/clang11      1.28GB/s ± 0%  1.30GB/s ± 0%   +1.45%  (p=0.008 n=5+5)

wuffs_gif_decode_1k_bw/gcc10                  595MB/s ± 0%   642MB/s ± 0%   +7.91%  (p=0.008 n=5+5)
wuffs_gif_decode_1k_color_full_init/gcc10     168MB/s ± 0%   169MB/s ± 0%   +0.36%  (p=0.008 n=5+5)
wuffs_gif_decode_1k_color_part_init/gcc10     202MB/s ± 0%   203MB/s ± 0%   +0.46%  (p=0.008 n=5+5)
wuffs_gif_decode_10k_bgra/gcc10               790MB/s ± 0%   798MB/s ± 0%   +1.04%  (p=0.008 n=5+5)
wuffs_gif_decode_10k_indexed/gcc10            208MB/s ± 0%   210MB/s ± 0%   +1.00%  (p=0.008 n=5+5)
wuffs_gif_decode_20k/gcc10                    258MB/s ± 0%   266MB/s ± 0%   +3.08%  (p=0.008 n=5+5)
wuffs_gif_decode_100k_artificial/gcc10        566MB/s ± 0%   577MB/s ± 0%   +1.83%  (p=0.008 n=5+5)
wuffs_gif_decode_100k_realistic/gcc10         220MB/s ± 0%   227MB/s ± 0%   +3.03%  (p=0.008 n=5+5)
wuffs_gif_decode_1000k_full_init/gcc10        223MB/s ± 0%   231MB/s ± 0%   +3.23%  (p=0.008 n=5+5)
wuffs_gif_decode_1000k_part_init/gcc10        224MB/s ± 0%   231MB/s ± 0%   +3.14%  (p=0.008 n=5+5)
wuffs_gif_decode_anim_screencap/gcc10        1.30GB/s ± 0%  1.32GB/s ± 0%   +1.70%  (p=0.008 n=5+5)

wuffs_lzw_decode_20k/clang11                  294MB/s ± 0%   321MB/s ± 0%   +9.11%  (p=0.008 n=5+5)
wuffs_lzw_decode_100k/clang11                 533MB/s ± 0%   560MB/s ± 0%   +5.13%  (p=0.008 n=5+5)

wuffs_lzw_decode_20k/gcc10                    271MB/s ± 0%   272MB/s ± 0%   +0.55%  (p=0.008 n=5+5)
wuffs_lzw_decode_100k/gcc10                   527MB/s ± 0%   521MB/s ± 0%   -1.31%  (p=0.008 n=5+5)

Updates #96
diff --git a/doc/changelog.md b/doc/changelog.md
index bf6f0ca..fbb05c2 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -1,6 +1,13 @@
 # Changelog
 
 
+## Work In Progress
+
+For a *closed* `io_reader`, the standard library now returns `"#truncated
+input"` instead of `"$short read"`. Importantly, this is an error, not a
+suspension.
+
+
 ## 2023-01-26 version 0.3.0
 
 The headline feature is that we have a production quality PNG decoder. It's
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index 27c69b5..547c368 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -7843,6 +7843,7 @@
 // ---------------- Status Codes
 
 extern const char wuffs_lzw__error__bad_code[];
+extern const char wuffs_lzw__error__truncated_input[];
 
 // ---------------- Public Consts
 
@@ -30680,6 +30681,7 @@
 // ---------------- Status Codes Implementations
 
 const char wuffs_lzw__error__bad_code[] = "#lzw: bad code";
+const char wuffs_lzw__error__truncated_input[] = "#lzw: truncated input";
 const char wuffs_lzw__error__internal_error_inconsistent_i_o[] = "#lzw: internal error: inconsistent I/O";
 
 // ---------------- Private Consts
@@ -30902,6 +30904,9 @@
         status = wuffs_base__make_status(wuffs_base__suspension__short_read);
         WUFFS_BASE__COROUTINE_SUSPENSION_POINT_MAYBE_SUSPEND(2);
       } else if (self->private_impl.f_read_from_return_value == 3) {
+        status = wuffs_base__make_status(wuffs_lzw__error__truncated_input);
+        goto exit;
+      } else if (self->private_impl.f_read_from_return_value == 4) {
         status = wuffs_base__make_status(wuffs_lzw__error__bad_code);
         goto exit;
       } else {
@@ -30977,7 +30982,11 @@
         iop_a_src += ((31 - v_n_bits) >> 3);
         v_n_bits |= 24;
       } else if (((uint64_t)(io2_a_src - iop_a_src)) <= 0) {
-        self->private_impl.f_read_from_return_value = 2;
+        if (a_src && a_src->meta.closed) {
+          self->private_impl.f_read_from_return_value = 3;
+        } else {
+          self->private_impl.f_read_from_return_value = 2;
+        }
         goto label__0__break;
       } else {
         v_bits |= (((uint32_t)(wuffs_base__peek_u8be__no_bounds_check(iop_a_src))) << v_n_bits);
@@ -30985,14 +30994,18 @@
         v_n_bits += 8;
         if (v_n_bits >= v_width) {
         } else if (((uint64_t)(io2_a_src - iop_a_src)) <= 0) {
-          self->private_impl.f_read_from_return_value = 2;
+          if (a_src && a_src->meta.closed) {
+            self->private_impl.f_read_from_return_value = 3;
+          } else {
+            self->private_impl.f_read_from_return_value = 2;
+          }
           goto label__0__break;
         } else {
           v_bits |= (((uint32_t)(wuffs_base__peek_u8be__no_bounds_check(iop_a_src))) << v_n_bits);
           iop_a_src += 1;
           v_n_bits += 8;
           if (v_n_bits < v_width) {
-            self->private_impl.f_read_from_return_value = 4;
+            self->private_impl.f_read_from_return_value = 5;
             goto label__0__break;
           }
         }
@@ -31070,7 +31083,7 @@
         v_prev_code = v_code;
       }
     } else {
-      self->private_impl.f_read_from_return_value = 3;
+      self->private_impl.f_read_from_return_value = 4;
       goto label__0__break;
     }
     if (v_output_wi > 4095) {
@@ -31085,7 +31098,7 @@
       if (iop_a_src > io1_a_src) {
         iop_a_src--;
       } else {
-        self->private_impl.f_read_from_return_value = 4;
+        self->private_impl.f_read_from_return_value = 5;
         goto label__2__break;
       }
     }
diff --git a/std/lzw/decode_lzw.wuffs b/std/lzw/decode_lzw.wuffs
index b653eb7..b193900 100644
--- a/std/lzw/decode_lzw.wuffs
+++ b/std/lzw/decode_lzw.wuffs
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 pub status "#bad code"
+pub status "#truncated input"
 
 pri status "#internal error: inconsistent I/O"
 
@@ -119,6 +120,8 @@
 		} else if this.read_from_return_value == 2 {
 			yield? base."$short read"
 		} else if this.read_from_return_value == 3 {
+			return "#truncated input"
+		} else if this.read_from_return_value == 4 {
 			return "#bad code"
 		} else {
 			return "#internal error: inconsistent I/O"
@@ -167,7 +170,11 @@
 				assert width <= n_bits via "a <= b: a <= c; c <= b"(c: 12)
 				assert n_bits >= width via "a >= b: b <= a"()
 			} else if args.src.length() <= 0 {
-				this.read_from_return_value = 2
+				if args.src.is_closed() {
+					this.read_from_return_value = 3
+				} else {
+					this.read_from_return_value = 2
+				}
 				break
 			} else {
 				bits |= args.src.peek_u8_as_u32() << n_bits
@@ -176,7 +183,11 @@
 				if n_bits >= width {
 					// No-op.
 				} else if args.src.length() <= 0 {
-					this.read_from_return_value = 2
+					if args.src.is_closed() {
+						this.read_from_return_value = 3
+					} else {
+						this.read_from_return_value = 2
+					}
 					break
 				} else {
 					bits |= args.src.peek_u8_as_u32() << n_bits
@@ -188,7 +199,7 @@
 					// This if condition is always false, but for some unknown
 					// reason, removing it worsens the benchmarks slightly.
 					if n_bits < width {
-						this.read_from_return_value = 4
+						this.read_from_return_value = 5
 						break
 					}
 				}
@@ -297,7 +308,7 @@
 			}
 
 		} else {
-			this.read_from_return_value = 3
+			this.read_from_return_value = 4
 			break
 		}
 
@@ -319,7 +330,7 @@
 			if args.src.can_undo_byte() {
 				args.src.undo_byte!()
 			} else {
-				this.read_from_return_value = 4
+				this.read_from_return_value = 5
 				break
 			}
 		} endwhile
diff --git a/test/c/std/lzw.c b/test/c/std/lzw.c
index 6cfcd31..a18faaa 100644
--- a/test/c/std/lzw.c
+++ b/test/c/std/lzw.c
@@ -80,6 +80,35 @@
 }
 
 const char*  //
+test_wuffs_lzw_decode_truncated_input() {
+  CHECK_FOCUS(__func__);
+
+  wuffs_base__io_buffer have = wuffs_base__ptr_u8__writer(g_have_array_u8, 1);
+  wuffs_base__io_buffer src =
+      wuffs_base__ptr_u8__reader(g_src_array_u8, 0, false);
+  wuffs_lzw__decoder dec;
+  CHECK_STATUS("initialize",
+               wuffs_lzw__decoder__initialize(
+                   &dec, sizeof dec, WUFFS_VERSION,
+                   WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED));
+
+  wuffs_base__status status =
+      wuffs_lzw__decoder__transform_io(&dec, &have, &src, g_work_slice_u8);
+  if (status.repr != wuffs_base__suspension__short_read) {
+    RETURN_FAIL("closed=false: have \"%s\", want \"%s\"", status.repr,
+                wuffs_base__suspension__short_read);
+  }
+
+  src.meta.closed = true;
+  status = wuffs_lzw__decoder__transform_io(&dec, &have, &src, g_work_slice_u8);
+  if (status.repr != wuffs_lzw__error__truncated_input) {
+    RETURN_FAIL("closed=true: have \"%s\", want \"%s\"", status.repr,
+                wuffs_lzw__error__truncated_input);
+  }
+  return NULL;
+}
+
+const char*  //
 do_test_wuffs_lzw_decode(const char* src_filename,
                          uint64_t src_size,
                          const char* want_filename,
@@ -444,6 +473,7 @@
     test_wuffs_lzw_decode_output_bad,
     test_wuffs_lzw_decode_output_empty,
     test_wuffs_lzw_decode_pi,
+    test_wuffs_lzw_decode_truncated_input,
     test_wuffs_lzw_decode_width_0,
     test_wuffs_lzw_decode_width_1,