Rename `[i..j]` to `[i ..= j]`, the same as Rust
diff --git a/README.md b/README.md
index 5dd3443..0e61817 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@
 
 ```
 $ wuffs gen std/gif
-check: expression "(c + 1) as u8" bounds [1..256] is not within bounds [0..255] at
+check: expression "(c + 1) as u8" bounds [1 ..= 256] is not within bounds [0 ..= 255] at
 /home/n/go/src/github.com/google/wuffs/std/lzw/decode_lzw.wuffs:101. Facts:
     n_bits < 8
     c < 256
@@ -250,8 +250,8 @@
 should fail, as an `n_bits < 8` assertion, a pre-condition, a few lines further
 down again cannot be proven.
 
-Similarly, changing the `4095` in `var prev_code u32[..4095]` either higher or
-lower should fail.
+Similarly, changing the `4095` in `var prev_code u32[..= 4095]` either higher
+or lower should fail.
 
 Try adding `assert false` at various places, which should obviously fail, but
 should also cause `wuffs gen` to print what facts the compiler can prove at
@@ -357,4 +357,4 @@
 
 ---
 
-Updated on June 2018.
+Updated on October 2019.
diff --git a/cmd/wuffs-c/test.go b/cmd/wuffs-c/test.go
index 3e13fc1..7ba7031 100644
--- a/cmd/wuffs-c/test.go
+++ b/cmd/wuffs-c/test.go
@@ -51,11 +51,11 @@
 		return fmt.Errorf("bad -focus flag value %q", *focusFlag)
 	}
 	if *iterscaleFlag < cf.IterscaleMin || cf.IterscaleMax < *iterscaleFlag {
-		return fmt.Errorf("bad -iterscale flag value %d, outside the range [%d..%d]",
+		return fmt.Errorf("bad -iterscale flag value %d, outside the range [%d ..= %d]",
 			*iterscaleFlag, cf.IterscaleMin, cf.IterscaleMax)
 	}
 	if *repsFlag < cf.RepsMin || cf.RepsMax < *repsFlag {
-		return fmt.Errorf("bad -reps flag value %d, outside the range [%d..%d]",
+		return fmt.Errorf("bad -reps flag value %d, outside the range [%d ..= %d]",
 			*repsFlag, cf.RepsMin, cf.RepsMax)
 	}
 
diff --git a/cmd/wuffs/test.go b/cmd/wuffs/test.go
index 75e09ed..fd85e3d 100644
--- a/cmd/wuffs/test.go
+++ b/cmd/wuffs/test.go
@@ -58,11 +58,11 @@
 		return fmt.Errorf("bad -focus flag value %q", *focusFlag)
 	}
 	if *iterscaleFlag < cf.IterscaleMin || cf.IterscaleMax < *iterscaleFlag {
-		return fmt.Errorf("bad -iterscale flag value %d, outside the range [%d..%d]",
+		return fmt.Errorf("bad -iterscale flag value %d, outside the range [%d ..= %d]",
 			*iterscaleFlag, cf.IterscaleMin, cf.IterscaleMax)
 	}
 	if *repsFlag < cf.RepsMin || cf.RepsMax < *repsFlag {
-		return fmt.Errorf("bad -reps flag value %d, outside the range [%d..%d]",
+		return fmt.Errorf("bad -reps flag value %d, outside the range [%d ..= %d]",
 			*repsFlag, cf.RepsMin, cf.RepsMax)
 	}
 
diff --git a/doc/changelog.md b/doc/changelog.md
index 751bd94..3d77413 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -36,6 +36,7 @@
 - Renamed `~+` to `~mod+`; added `~mod-`, `~sat+` and `~sat-`.
 - Removed `&^`.
 - Renamed `$(etc)` to `[etc]`.
+- Renamed `[i..j]` to `[i ..= j]`, consistent with Rust syntax.
 - Renamed `[N] T` and `[] T` types to `array[N] T` and `slice T`.
 - Renamed `u32`, `buf1`, etc to `base.u32`, `base.io_buffer`, etc.
 - Renamed `unread_u8?` to `undo_byte!`; added `can_undo_byte`.
diff --git a/lang/ast/ast.go b/lang/ast/ast.go
index eac9489..7edc0b2 100644
--- a/lang/ast/ast.go
+++ b/lang/ast/ast.go
@@ -636,7 +636,7 @@
 // MaxTypeExprDepth is an advisory limit for a TypeExpr's recursion depth.
 const MaxTypeExprDepth = 63
 
-// TypeExpr is a type expression, such as "base.u32", "base.u32[..8]", "foo",
+// TypeExpr is a type expression, such as "base.u32", "base.u32[..= 8]", "foo",
 // "pkg.bar", "ptr T", "array[8] T", "slice T" or "table T":
 //  - ID0:   <0|IDArray|IDFunc|IDNptr|IDPtr|IDSlice|IDTable>
 //  - ID1:   <0|pkg>
@@ -662,8 +662,8 @@
 // A zero ID0 means a (possibly package-qualified) type like "pkg.foo" or
 // "foo". ID1 is the "pkg" or zero, ID2 is the "foo".
 //
-// Numeric types can be refined as "foo[LHS..MHS]". LHS and MHS are Expr's,
-// possibly nil. For example, the LHS for "base.u32[..4095]" is nil.
+// Numeric types can be refined as "foo[LHS ..= MHS]". LHS and MHS are Expr's,
+// possibly nil. For example, the LHS for "base.u32[..= 4095]" is nil.
 //
 // TODO: struct types, list types, nptr vs ptr.
 type TypeExpr Node
diff --git a/lang/ast/string.go b/lang/ast/string.go
index 618e28b..b5186cb 100644
--- a/lang/ast/string.go
+++ b/lang/ast/string.go
@@ -234,9 +234,15 @@
 	}
 	if n.Min() != nil || n.Max() != nil {
 		buf = append(buf, '[')
-		buf = n.Min().appendStr(buf, tm, false, 0)
-		buf = append(buf, ".."...)
-		buf = n.Max().appendStr(buf, tm, false, 0)
+		if n.Min() != nil {
+			buf = n.Min().appendStr(buf, tm, false, 0)
+			buf = append(buf, ' ')
+		}
+		buf = append(buf, "..="...)
+		if n.Max() != nil {
+			buf = append(buf, ' ')
+			buf = n.Max().appendStr(buf, tm, false, 0)
+		}
 		buf = append(buf, ']')
 	}
 	return buf
diff --git a/lang/ast/string_test.go b/lang/ast/string_test.go
index c34ad8f..68ae72a 100644
--- a/lang/ast/string_test.go
+++ b/lang/ast/string_test.go
@@ -62,13 +62,13 @@
 		"x as base.u32",
 		"x as T",
 		"x as T",
-		"x as T[i..]",
-		"x as T[..j]",
-		"x as T[i..j]",
+		"x as T[i ..=]",
+		"x as T[..= j]",
+		"x as T[i ..= j]",
 		"x as pkg.T",
 		"x as ptr T",
 		"x as array[4] T",
-		"x as array[8 + (2 * N)] ptr array[4] ptr pkg.T[i..j]",
+		"x as array[8 + (2 * N)] ptr array[4] ptr pkg.T[i ..= j]",
 	}
 
 	tm := &t.Map{}
diff --git a/lang/builtin/builtin.go b/lang/builtin/builtin.go
index 8dfab7a..d32d686 100644
--- a/lang/builtin/builtin.go
+++ b/lang/builtin/builtin.go
@@ -88,23 +88,23 @@
 }
 
 var Funcs = []string{
-	"u8.high_bits(n u32[..8]) u8",
-	"u8.low_bits(n u32[..8]) u8",
+	"u8.high_bits(n u32[..= 8]) u8",
+	"u8.low_bits(n u32[..= 8]) u8",
 	"u8.max(a u8) u8",
 	"u8.min(a u8) u8",
 
-	"u16.high_bits(n u32[..16]) u16",
-	"u16.low_bits(n u32[..16]) u16",
+	"u16.high_bits(n u32[..= 16]) u16",
+	"u16.low_bits(n u32[..= 16]) u16",
 	"u16.max(a u16) u16",
 	"u16.min(a u16) u16",
 
-	"u32.high_bits(n u32[..32]) u32",
-	"u32.low_bits(n u32[..32]) u32",
+	"u32.high_bits(n u32[..= 32]) u32",
+	"u32.low_bits(n u32[..= 32]) u32",
 	"u32.max(a u32) u32",
 	"u32.min(a u32) u32",
 
-	"u64.high_bits(n u32[..64]) u64",
-	"u64.low_bits(n u32[..64]) u64",
+	"u64.high_bits(n u32[..= 64]) u64",
+	"u64.low_bits(n u32[..= 64]) u64",
 	"u64.max(a u64) u64",
 	"u64.min(a u64) u64",
 
@@ -156,27 +156,27 @@
 	"io_reader.read_u16be?() u16",
 	"io_reader.read_u16le?() u16",
 
-	"io_reader.read_u8_as_u32?() u32[..0xFF]",
-	"io_reader.read_u16be_as_u32?() u32[..0xFFFF]",
-	"io_reader.read_u16le_as_u32?() u32[..0xFFFF]",
-	"io_reader.read_u24be_as_u32?() u32[..0xFFFFFF]",
-	"io_reader.read_u24le_as_u32?() u32[..0xFFFFFF]",
+	"io_reader.read_u8_as_u32?() u32[..= 0xFF]",
+	"io_reader.read_u16be_as_u32?() u32[..= 0xFFFF]",
+	"io_reader.read_u16le_as_u32?() u32[..= 0xFFFF]",
+	"io_reader.read_u24be_as_u32?() u32[..= 0xFFFFFF]",
+	"io_reader.read_u24le_as_u32?() u32[..= 0xFFFFFF]",
 	"io_reader.read_u32be?() u32",
 	"io_reader.read_u32le?() u32",
 
-	"io_reader.read_u8_as_u64?() u64[..0xFF]",
-	"io_reader.read_u16be_as_u64?() u64[..0xFFFF]",
-	"io_reader.read_u16le_as_u64?() u64[..0xFFFF]",
-	"io_reader.read_u24be_as_u64?() u64[..0xFFFFFF]",
-	"io_reader.read_u24le_as_u64?() u64[..0xFFFFFF]",
-	"io_reader.read_u32be_as_u64?() u64[..0xFFFFFFFF]",
-	"io_reader.read_u32le_as_u64?() u64[..0xFFFFFFFF]",
-	"io_reader.read_u40be_as_u64?() u64[..0xFFFFFFFFFF]",
-	"io_reader.read_u40le_as_u64?() u64[..0xFFFFFFFFFF]",
-	"io_reader.read_u48be_as_u64?() u64[..0xFFFFFFFFFFFF]",
-	"io_reader.read_u48le_as_u64?() u64[..0xFFFFFFFFFFFF]",
-	"io_reader.read_u56be_as_u64?() u64[..0xFFFFFFFFFFFFFF]",
-	"io_reader.read_u56le_as_u64?() u64[..0xFFFFFFFFFFFFFF]",
+	"io_reader.read_u8_as_u64?() u64[..= 0xFF]",
+	"io_reader.read_u16be_as_u64?() u64[..= 0xFFFF]",
+	"io_reader.read_u16le_as_u64?() u64[..= 0xFFFF]",
+	"io_reader.read_u24be_as_u64?() u64[..= 0xFFFFFF]",
+	"io_reader.read_u24le_as_u64?() u64[..= 0xFFFFFF]",
+	"io_reader.read_u32be_as_u64?() u64[..= 0xFFFFFFFF]",
+	"io_reader.read_u32le_as_u64?() u64[..= 0xFFFFFFFF]",
+	"io_reader.read_u40be_as_u64?() u64[..= 0xFFFFFFFFFF]",
+	"io_reader.read_u40le_as_u64?() u64[..= 0xFFFFFFFFFF]",
+	"io_reader.read_u48be_as_u64?() u64[..= 0xFFFFFFFFFFFF]",
+	"io_reader.read_u48le_as_u64?() u64[..= 0xFFFFFFFFFFFF]",
+	"io_reader.read_u56be_as_u64?() u64[..= 0xFFFFFFFFFFFFFF]",
+	"io_reader.read_u56le_as_u64?() u64[..= 0xFFFFFFFFFFFFFF]",
 	"io_reader.read_u64be?() u64",
 	"io_reader.read_u64le?() u64",
 
@@ -192,27 +192,27 @@
 	"io_reader.peek_u16be() u16",
 	"io_reader.peek_u16le() u16",
 
-	"io_reader.peek_u8_as_u32() u32[..0xFF]",
-	"io_reader.peek_u16be_as_u32() u32[..0xFFFF]",
-	"io_reader.peek_u16le_as_u32() u32[..0xFFFF]",
-	"io_reader.peek_u24be_as_u32() u32[..0xFFFFFF]",
-	"io_reader.peek_u24le_as_u32() u32[..0xFFFFFF]",
+	"io_reader.peek_u8_as_u32() u32[..= 0xFF]",
+	"io_reader.peek_u16be_as_u32() u32[..= 0xFFFF]",
+	"io_reader.peek_u16le_as_u32() u32[..= 0xFFFF]",
+	"io_reader.peek_u24be_as_u32() u32[..= 0xFFFFFF]",
+	"io_reader.peek_u24le_as_u32() u32[..= 0xFFFFFF]",
 	"io_reader.peek_u32be() u32",
 	"io_reader.peek_u32le() u32",
 
-	"io_reader.peek_u8_as_u64() u64[..0xFF]",
-	"io_reader.peek_u16be_as_u64() u64[..0xFFFF]",
-	"io_reader.peek_u16le_as_u64() u64[..0xFFFF]",
-	"io_reader.peek_u24be_as_u64() u64[..0xFFFFFF]",
-	"io_reader.peek_u24le_as_u64() u64[..0xFFFFFF]",
-	"io_reader.peek_u32be_as_u64() u64[..0xFFFFFFFF]",
-	"io_reader.peek_u32le_as_u64() u64[..0xFFFFFFFF]",
-	"io_reader.peek_u40be_as_u64() u64[..0xFFFFFFFFFF]",
-	"io_reader.peek_u40le_as_u64() u64[..0xFFFFFFFFFF]",
-	"io_reader.peek_u48be_as_u64() u64[..0xFFFFFFFFFFFF]",
-	"io_reader.peek_u48le_as_u64() u64[..0xFFFFFFFFFFFF]",
-	"io_reader.peek_u56be_as_u64() u64[..0xFFFFFFFFFFFFFF]",
-	"io_reader.peek_u56le_as_u64() u64[..0xFFFFFFFFFFFFFF]",
+	"io_reader.peek_u8_as_u64() u64[..= 0xFF]",
+	"io_reader.peek_u16be_as_u64() u64[..= 0xFFFF]",
+	"io_reader.peek_u16le_as_u64() u64[..= 0xFFFF]",
+	"io_reader.peek_u24be_as_u64() u64[..= 0xFFFFFF]",
+	"io_reader.peek_u24le_as_u64() u64[..= 0xFFFFFF]",
+	"io_reader.peek_u32be_as_u64() u64[..= 0xFFFFFFFF]",
+	"io_reader.peek_u32le_as_u64() u64[..= 0xFFFFFFFF]",
+	"io_reader.peek_u40be_as_u64() u64[..= 0xFFFFFFFFFF]",
+	"io_reader.peek_u40le_as_u64() u64[..= 0xFFFFFFFFFF]",
+	"io_reader.peek_u48be_as_u64() u64[..= 0xFFFFFFFFFFFF]",
+	"io_reader.peek_u48le_as_u64() u64[..= 0xFFFFFFFFFFFF]",
+	"io_reader.peek_u56be_as_u64() u64[..= 0xFFFFFFFFFFFFFF]",
+	"io_reader.peek_u56le_as_u64() u64[..= 0xFFFFFFFFFFFFFF]",
 	"io_reader.peek_u64be() u64",
 	"io_reader.peek_u64le() u64",
 
@@ -236,16 +236,16 @@
 	"io_writer.write_u8?(a u8)",
 	"io_writer.write_u16be?(a u16)",
 	"io_writer.write_u16le?(a u16)",
-	"io_writer.write_u24be?(a u32[..0xFFFFFF])",
-	"io_writer.write_u24le?(a u32[..0xFFFFFF])",
+	"io_writer.write_u24be?(a u32[..= 0xFFFFFF])",
+	"io_writer.write_u24le?(a u32[..= 0xFFFFFF])",
 	"io_writer.write_u32be?(a u32)",
 	"io_writer.write_u32le?(a u32)",
-	"io_writer.write_u40be?(a u64[..0xFFFFFFFFFF])",
-	"io_writer.write_u40le?(a u64[..0xFFFFFFFFFF])",
-	"io_writer.write_u48be?(a u64[..0xFFFFFFFFFFFF])",
-	"io_writer.write_u48le?(a u64[..0xFFFFFFFFFFFF])",
-	"io_writer.write_u56be?(a u64[..0xFFFFFFFFFFFFFF])",
-	"io_writer.write_u56le?(a u64[..0xFFFFFFFFFFFFFF])",
+	"io_writer.write_u40be?(a u64[..= 0xFFFFFFFFFF])",
+	"io_writer.write_u40le?(a u64[..= 0xFFFFFFFFFF])",
+	"io_writer.write_u48be?(a u64[..= 0xFFFFFFFFFFFF])",
+	"io_writer.write_u48le?(a u64[..= 0xFFFFFFFFFFFF])",
+	"io_writer.write_u56be?(a u64[..= 0xFFFFFFFFFFFFFF])",
+	"io_writer.write_u56le?(a u64[..= 0xFFFFFFFFFFFFFF])",
 	"io_writer.write_u64be?(a u64)",
 	"io_writer.write_u64le?(a u64)",
 
@@ -257,16 +257,16 @@
 	"io_writer.write_fast_u8!(a u8)",
 	"io_writer.write_fast_u16be!(a u16)",
 	"io_writer.write_fast_u16le!(a u16)",
-	"io_writer.write_fast_u24be!(a u32[..0xFFFFFF])",
-	"io_writer.write_fast_u24le!(a u32[..0xFFFFFF])",
+	"io_writer.write_fast_u24be!(a u32[..= 0xFFFFFF])",
+	"io_writer.write_fast_u24le!(a u32[..= 0xFFFFFF])",
 	"io_writer.write_fast_u32be!(a u32)",
 	"io_writer.write_fast_u32le!(a u32)",
-	"io_writer.write_fast_u40be!(a u64[..0xFFFFFFFFFF])",
-	"io_writer.write_fast_u40le!(a u64[..0xFFFFFFFFFF])",
-	"io_writer.write_fast_u48be!(a u64[..0xFFFFFFFFFFFF])",
-	"io_writer.write_fast_u48le!(a u64[..0xFFFFFFFFFFFF])",
-	"io_writer.write_fast_u56be!(a u64[..0xFFFFFFFFFFFFFF])",
-	"io_writer.write_fast_u56le!(a u64[..0xFFFFFFFFFFFFFF])",
+	"io_writer.write_fast_u40be!(a u64[..= 0xFFFFFFFFFF])",
+	"io_writer.write_fast_u40le!(a u64[..= 0xFFFFFFFFFF])",
+	"io_writer.write_fast_u48be!(a u64[..= 0xFFFFFFFFFFFF])",
+	"io_writer.write_fast_u48le!(a u64[..= 0xFFFFFFFFFFFF])",
+	"io_writer.write_fast_u56be!(a u64[..= 0xFFFFFFFFFFFFFF])",
+	"io_writer.write_fast_u56le!(a u64[..= 0xFFFFFFFFFFFFFF])",
 	"io_writer.write_fast_u64be!(a u64)",
 	"io_writer.write_fast_u64le!(a u64)",
 
@@ -302,11 +302,11 @@
 
 	"frame_config.blend() u8",
 	"frame_config.disposal() u8",
-	"frame_config.duration() u64[..0x7FFFFFFFFFFFFFFF]",
+	"frame_config.duration() u64[..= 0x7FFFFFFFFFFFFFFF]",
 	"frame_config.index() u64",
 	"frame_config.io_position() u64",
 
-	"frame_config.update!(bounds rect_ie_u32, duration u64[..0x7FFFFFFFFFFFFFFF], " +
+	"frame_config.update!(bounds rect_ie_u32, duration u64[..= 0x7FFFFFFFFFFFFFFF], " +
 		"index u64, io_position u64, blend u8, disposal u8, background_color u32)",
 
 	// ---- image_config
@@ -318,7 +318,7 @@
 
 	"pixel_buffer.palette() slice u8",
 	"pixel_buffer.pixel_format() u32",
-	"pixel_buffer.plane(p u32[..3]) table u8",
+	"pixel_buffer.plane(p u32[..= 3]) table u8",
 
 	// ---- pixel_swizzler
 
diff --git a/lang/check/check_test.go b/lang/check/check_test.go
index d80cc9e..a5cb210 100644
--- a/lang/check/check_test.go
+++ b/lang/check/check_test.go
@@ -66,12 +66,12 @@
 		pri func foo.bar() {
 			var x base.u8
 			var y base.i32
-			var z base.u64[..123]
+			var z base.u64[..= 123]
 			var a array[4] base.u8
 			var b base.bool
 
 			var p base.i32
-			var q base.i32[0..8]
+			var q base.i32[0 ..= 8]
 
 			x = 0
 			x = 1 + (x * 0)
@@ -158,11 +158,11 @@
 		{"b", "base.bool"},
 		{"coroutine_resumed", "base.bool"},
 		{"p", "base.i32"},
-		{"q", "base.i32[0..8]"},
+		{"q", "base.i32[0 ..= 8]"},
 		{"this", "ptr foo"},
 		{"x", "base.u8"},
 		{"y", "base.i32"},
-		{"z", "base.u64[..123]"},
+		{"z", "base.u64[..= 123]"},
 	}
 	if !reflect.DeepEqual(got, want) {
 		tt.Fatalf("\ngot  %v\nwant %v", got, want)
diff --git a/lang/parse/parse.go b/lang/parse/parse.go
index 89f6468..bb2e1c4 100644
--- a/lang/parse/parse.go
+++ b/lang/parse/parse.go
@@ -432,7 +432,7 @@
 
 	lhs, mhs := (*a.Expr)(nil), (*a.Expr)(nil)
 	if p.peek1() == t.IDOpenBracket {
-		_, lhs, mhs, err = p.parseBracket(t.IDDotDot)
+		_, lhs, mhs, err = p.parseBracket(t.IDDotDotEq)
 		if err != nil {
 			return nil, err
 		}
@@ -441,8 +441,8 @@
 	return a.NewTypeExpr(0, pkg, name, lhs.AsNode(), mhs, nil), nil
 }
 
-// parseBracket parses "[i:j]", "[i:]", "[:j]" and "[:]". A double dot replaces
-// the colon if sep is t.IDDotDot instead of t.IDColon. If sep is t.IDColon, it
+// parseBracket parses "[i:j]", "[i:]", "[:j]" and "[:]". A "..=" replaces the
+// ":" if sep is t.IDDotDotEq instead of t.IDColon. If sep is t.IDColon, it
 // also parses "[x]". The returned op is sep for a range or refinement and
 // t.IDOpenBracket for an index.
 func (p *parser) parseBracket(sep t.ID) (op t.ID, ei *a.Expr, ej *a.Expr, err error) {
@@ -934,7 +934,7 @@
 
 	length := p.peek1()
 	if length.SmallPowerOf2Value() == 0 {
-		return nil, fmt.Errorf(`parse: expected power-of-2 length count in [1..256], got %q at %s:%d`,
+		return nil, fmt.Errorf(`parse: expected power-of-2 length count in [1 ..= 256], got %q at %s:%d`,
 			p.tm.ByID(length), p.filename, p.line())
 	}
 	p.src = p.src[1:]
@@ -959,7 +959,7 @@
 
 	unroll := p.peek1()
 	if unroll.SmallPowerOf2Value() == 0 {
-		return nil, fmt.Errorf(`parse: expected power-of-2 unroll count in [1..256], got %q at %s:%d`,
+		return nil, fmt.Errorf(`parse: expected power-of-2 unroll count in [1 ..= 256], got %q at %s:%d`,
 			p.tm.ByID(unroll), p.filename, p.line())
 	}
 	p.src = p.src[1:]
diff --git a/lang/token/list.go b/lang/token/list.go
index a99667d..a2df3da 100644
--- a/lang/token/list.go
+++ b/lang/token/list.go
@@ -201,7 +201,8 @@
 )
 
 const (
-	IDInvalid = ID(0)
+	IDInvalid   = ID(0)
+	IDSemicolon = ID(0x01)
 
 	minOpen = 0x02
 	maxOpen = 0x04
@@ -217,14 +218,14 @@
 	IDCloseBracket = ID(0x06)
 	IDCloseCurly   = ID(0x07)
 
-	IDDot       = ID(0x08)
-	IDDotDot    = ID(0x09)
-	IDComma     = ID(0x0A)
-	IDExclam    = ID(0x0B)
-	IDQuestion  = ID(0x0C)
-	IDColon     = ID(0x0D)
-	IDSemicolon = ID(0x0E)
-	IDDollar    = ID(0x0F)
+	IDDot      = ID(0x08)
+	IDDotDot   = ID(0x09)
+	IDDotDotEq = ID(0x0A)
+	IDComma    = ID(0x0B)
+	IDExclam   = ID(0x0C)
+	IDQuestion = ID(0x0D)
+	IDColon    = ID(0x0E)
+	IDDollar   = ID(0x0F)
 )
 
 const (
@@ -614,6 +615,7 @@
 
 	IDDot:       ".",
 	IDDotDot:    "..",
+	IDDotDotEq:  "..=",
 	IDComma:     ",",
 	IDExclam:    "!",
 	IDQuestion:  "?",
@@ -971,6 +973,7 @@
 // we want to lex greedily, longer suffixes should be earlier in the slice.
 var lexers = [256][]suffixLexer{
 	'.': {
+		{".=", IDDotDotEq},
 		{".", IDDotDot},
 		{"", IDDot},
 	},
@@ -1165,17 +1168,18 @@
 }
 
 var isTightLeft = [...]bool{
+	IDSemicolon: true,
+
 	IDCloseParen:   true,
 	IDOpenBracket:  true,
 	IDCloseBracket: true,
 
-	IDDot:       true,
-	IDDotDot:    true,
-	IDComma:     true,
-	IDExclam:    true,
-	IDQuestion:  true,
-	IDColon:     true,
-	IDSemicolon: true,
+	IDDot:      true,
+	IDDotDot:   true,
+	IDComma:    true,
+	IDExclam:   true,
+	IDQuestion: true,
+	IDColon:    true,
 }
 
 var isTightRight = [...]bool{
diff --git a/lib/interval/interval.go b/lib/interval/interval.go
index e6f7899..2d5c8b4 100644
--- a/lib/interval/interval.go
+++ b/lib/interval/interval.go
@@ -150,15 +150,15 @@
 	}
 	buf := []byte(nil)
 	if x[0] == nil {
-		buf = append(buf, "(-∞, "...)
+		buf = append(buf, "(-∞"...)
 	} else {
 		buf = append(buf, '[')
 		buf = x[0].Append(buf, 10)
-		buf = append(buf, ".."...)
 	}
 	if x[1] == nil {
-		buf = append(buf, "+∞)"...)
+		buf = append(buf, " .. +∞)"...)
 	} else {
+		buf = append(buf, " ..= "...)
 		buf = x[1].Append(buf, 10)
 		buf = append(buf, ']')
 	}
diff --git a/std/deflate/common_consts.wuffs b/std/deflate/common_consts.wuffs
index f67d3b7..86a3a81 100644
--- a/std/deflate/common_consts.wuffs
+++ b/std/deflate/common_consts.wuffs
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 // code_order is defined in the RFC section 3.2.7.
-pri const code_order array[19] base.u8[..18] = [
+pri const code_order array[19] base.u8[..= 18] = [
 	16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15,
 ]
 
diff --git a/std/deflate/decode_deflate.wuffs b/std/deflate/decode_deflate.wuffs
index b3e02e9..9592d88 100644
--- a/std/deflate/decode_deflate.wuffs
+++ b/std/deflate/decode_deflate.wuffs
@@ -71,7 +71,7 @@
 	history_index base.u32,
 
 	// n_huffs_bits is discussed in the huffs field comment.
-	n_huffs_bits array[2] base.u32[..9],
+	n_huffs_bits array[2] base.u32[..= 9],
 
 	// end_of_block is whether decode_huffman_xxx saw an end-of-block code.
 	//
@@ -99,17 +99,19 @@
 	// less, then more source bytes are read and the table lookup re-tried.
 	//
 	// The table value's bits:
-	//  - bit        31 indicates a literal.
-	//  - bit        30 indicates a base number + extra bits.
-	//  - bit        29 indicates end-of-block.
-	//  - bit        28 indicates a redirect to another part of the table.
-	//  - bit        27 indicates an invalid value.
-	//  - bits 26 .. 24 are zero.
-	//  - bits 23 ..  8 are the redirect offset, literal (in bits 15-8) or base number.
-	//  - bits  7 ..  4 are the redirected table's bits or number of extra bits.
-	//  - bits  3 ..  0 are the number of decoder.bits to consume.
+	//  - bit         31 indicates a literal.
+	//  - bit         30 indicates a base number + extra bits.
+	//  - bit         29 indicates end-of-block.
+	//  - bit         28 indicates a redirect to another part of the table.
+	//  - bit         27 indicates an invalid value.
+	//  - bits 24 ..= 26 are zero.
+	//  - bits  8 ..= 23 are the redirect offset, literal (in bits [8 ..= 15])
+	//                   or base number.
+	//  - bits  4 ..=  7 are the redirected table's size in bits or the number
+	//                   of extra bits.
+	//  - bits  0 ..=  3 are the number of decoder.bits to consume.
 	//
-	// Exactly one of the eight bits 31-24 should be set.
+	// Exactly one of the eight bits [24 ..= 31] should be set.
 	huffs array[2] array[huffs_table_size] base.u32,
 
 	// history holds up to the last 32KiB of decoded output, if the decoding
@@ -135,7 +137,7 @@
 pub func decoder.add_history!(hist slice base.u8) {
 	var s            slice base.u8
 	var n_copied     base.u64
-	var already_full base.u32[..0x8000]
+	var already_full base.u32[..= 0x8000]
 
 	s = args.hist
 	if s.length() >= 0x8000 {
@@ -201,7 +203,7 @@
 
 pri func decoder.decode_blocks?(dst base.io_writer, src base.io_reader) {
 	var final  base.u32
-	var b0     base.u32[..255]
+	var b0     base.u32[..= 255]
 	var type   base.u32
 	var status base.status
 
@@ -330,21 +332,21 @@
 pri func decoder.init_dynamic_huffman?(src base.io_reader) {
 	var bits               base.u32
 	var n_bits             base.u32
-	var b0                 base.u32[..255]
-	var n_lit              base.u32[..288]
-	var n_dist             base.u32[..32]
-	var n_clen             base.u32[..19]
+	var b0                 base.u32[..= 255]
+	var n_lit              base.u32[..= 288]
+	var n_dist             base.u32[..= 32]
+	var n_clen             base.u32[..= 19]
 	var i                  base.u32
-	var b1                 base.u32[..255]
+	var b1                 base.u32[..= 255]
 	var status             base.status
-	var mask               base.u32[..511]
+	var mask               base.u32[..= 511]
 	var table_entry        base.u32
-	var table_entry_n_bits base.u32[..15]
-	var b2                 base.u32[..255]
-	var n_extra_bits       base.u32[..7]
-	var rep_symbol         base.u8[..15]
+	var table_entry_n_bits base.u32[..= 15]
+	var b2                 base.u32[..= 255]
+	var n_extra_bits       base.u32[..= 7]
+	var rep_symbol         base.u8[..= 15]
 	var rep_count          base.u32
-	var b3                 base.u32[..255]
+	var b3                 base.u32[..= 255]
 
 	bits = this.bits
 	n_bits = this.n_bits
@@ -504,29 +506,29 @@
 
 // TODO: make named constants for 15, 19, 319, etc.
 
-pri func decoder.init_huff!(which base.u32[..1], n_codes0 base.u32[..288], n_codes1 base.u32[..320], base_symbol base.u32) base.status {
-	var counts            array[16] base.u16[..320]
+pri func decoder.init_huff!(which base.u32[..= 1], n_codes0 base.u32[..= 288], n_codes1 base.u32[..= 320], base_symbol base.u32) base.status {
+	var counts            array[16] base.u16[..= 320]
 	var i                 base.u32
 	var remaining         base.u32
-	var offsets           array[16] base.u16[..320]
-	var n_symbols         base.u32[..320]
-	var count             base.u32[..320]
-	var symbols           array[320] base.u16[..319]
-	var min_cl            base.u32[..9]
-	var max_cl            base.u32[..15]
+	var offsets           array[16] base.u16[..= 320]
+	var n_symbols         base.u32[..= 320]
+	var count             base.u32[..= 320]
+	var symbols           array[320] base.u16[..= 319]
+	var min_cl            base.u32[..= 9]
+	var max_cl            base.u32[..= 15]
 	var initial_high_bits base.u32
-	var prev_cl           base.u32[..15]
+	var prev_cl           base.u32[..= 15]
 	var prev_redirect_key base.u32
-	var top               base.u32[..huffs_table_size]
-	var next_top          base.u32[..huffs_table_size]
+	var top               base.u32[..= huffs_table_size]
+	var next_top          base.u32[..= huffs_table_size]
 	var code              base.u32
 	var key               base.u32
 	var value             base.u32
-	var cl                base.u32[..15]
-	var redirect_key      base.u32[..511]
-	var j                 base.u32[..16]
-	var reversed_key      base.u32[..511]
-	var symbol            base.u32[..319]
+	var cl                base.u32[..= 15]
+	var redirect_key      base.u32[..= 511]
+	var j                 base.u32[..= 16]
+	var reversed_key      base.u32[..= 511]
+	var symbol            base.u32[..= 319]
 	var high_bits         base.u32
 	var delta             base.u32
 
diff --git a/std/deflate/decode_huffman_fast.wuffs b/std/deflate/decode_huffman_fast.wuffs
index f80d87a..50c50dc 100644
--- a/std/deflate/decode_huffman_fast.wuffs
+++ b/std/deflate/decode_huffman_fast.wuffs
@@ -23,15 +23,15 @@
 	var bits               base.u32
 	var n_bits             base.u32
 	var table_entry        base.u32
-	var table_entry_n_bits base.u32[..15]
-	var lmask              base.u32[..511]
-	var dmask              base.u32[..511]
-	var redir_top          base.u32[..0xFFFF]
-	var redir_mask         base.u32[..0x7FFF]
-	var length             base.u32[..258]
-	var dist_minus_1       base.u32[..0x7FFF]
+	var table_entry_n_bits base.u32[..= 15]
+	var lmask              base.u32[..= 511]
+	var dmask              base.u32[..= 511]
+	var redir_top          base.u32[..= 0xFFFF]
+	var redir_mask         base.u32[..= 0x7FFF]
+	var length             base.u32[..= 258]
+	var dist_minus_1       base.u32[..= 0x7FFF]
 	var n_copied           base.u32
-	var hlen               base.u32[..0x7FFF]
+	var hlen               base.u32[..= 0x7FFF]
 	var hdist              base.u32
 
 	if (this.n_bits >= 8) or ((this.bits >> (this.n_bits & 7)) <> 0) {
diff --git a/std/deflate/decode_huffman_slow.wuffs b/std/deflate/decode_huffman_slow.wuffs
index 0dc3c95..c35e6f4 100644
--- a/std/deflate/decode_huffman_slow.wuffs
+++ b/std/deflate/decode_huffman_slow.wuffs
@@ -16,21 +16,21 @@
 	var bits               base.u32
 	var n_bits             base.u32
 	var table_entry        base.u32
-	var table_entry_n_bits base.u32[..15]
-	var lmask              base.u32[..511]
-	var dmask              base.u32[..511]
-	var b0                 base.u32[..255]
-	var redir_top          base.u32[..0xFFFF]
-	var redir_mask         base.u32[..0x7FFF]
-	var b1                 base.u32[..255]
-	var length             base.u32[..258]
-	var b2                 base.u32[..255]
-	var b3                 base.u32[..255]
-	var b4                 base.u32[..255]
-	var dist_minus_1       base.u32[..0x7FFF]
-	var b5                 base.u32[..255]
+	var table_entry_n_bits base.u32[..= 15]
+	var lmask              base.u32[..= 511]
+	var dmask              base.u32[..= 511]
+	var b0                 base.u32[..= 255]
+	var redir_top          base.u32[..= 0xFFFF]
+	var redir_mask         base.u32[..= 0x7FFF]
+	var b1                 base.u32[..= 255]
+	var length             base.u32[..= 258]
+	var b2                 base.u32[..= 255]
+	var b3                 base.u32[..= 255]
+	var b4                 base.u32[..= 255]
+	var dist_minus_1       base.u32[..= 0x7FFF]
+	var b5                 base.u32[..= 255]
 	var n_copied           base.u32
-	var hlen               base.u32[..0x7FFF]
+	var hlen               base.u32[..= 0x7FFF]
 	var hdist              base.u32
 
 	// When editing this function, consider making the equivalent change to the
diff --git a/std/gif/decode_gif.wuffs b/std/gif/decode_gif.wuffs
index e42b588..1fa25fb 100644
--- a/std/gif/decode_gif.wuffs
+++ b/std/gif/decode_gif.wuffs
@@ -184,7 +184,7 @@
 	has_global_palette base.bool,
 
 	// interlace indexes the interlace_start and interlace_delta arrays.
-	interlace base.u8[..4],
+	interlace base.u8[..= 4],
 
 	// Absent an ANIMEXTS1.0 or NETSCAPE2.0 extension, the implicit number of
 	// animation loops is 1.
@@ -198,7 +198,7 @@
 	gc_transparent_index     base.u8,
 	gc_disposal              base.u8,
 	// There are 7056000 flicks per centisecond.
-	gc_duration base.u64[..0xFFFF * 7056000],
+	gc_duration base.u64[..= 0xFFFF * 7056000],
 
 	frame_config_io_position        base.u64,
 	num_decoded_frame_configs_value base.u64,
@@ -582,9 +582,9 @@
 pri func decoder.decode_lsd?(src base.io_reader) {
 	var flags                  base.u8
 	var background_color_index base.u8
-	var num_palette_entries    base.u32[..256]
+	var num_palette_entries    base.u32[..= 256]
 	var i                      base.u32
-	var j                      base.u32[..1020]
+	var j                      base.u32[..= 1020]
 	var argb                   base.u32
 
 	this.width = args.src.read_u16le_as_u32?()
@@ -828,7 +828,7 @@
 	// Convert the disposal method from GIF's wire format to Wuffs constants.
 	//
 	// The GIF spec discusses the 3-bit flag value being 0, 1, 2 or 3. Values
-	// in the range [4..7] are "to be defined". In practice, some encoders also
+	// in the range [4 ..= 7] are "to be defined". In practice, some encoders also
 	// use 4 for "restore previous". See
 	// https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/image-decoders/gif/gif_image_reader.cc?rcl=5161173c43324da2b13e1aa45bbe69901daa1279&l=625
 	//
@@ -889,8 +889,8 @@
 
 pri func decoder.decode_id_part1?(dst ptr base.pixel_buffer, src base.io_reader) {
 	var flags               base.u8
-	var which_palette       base.u8[..1]
-	var num_palette_entries base.u32[..256]
+	var which_palette       base.u8[..= 1]
+	var num_palette_entries base.u32[..= 256]
 	var i                   base.u32
 	var argb                base.u32
 	var dst_palette         slice base.u8
@@ -993,7 +993,7 @@
 }
 
 pri func decoder.decode_id_part2?(dst ptr base.pixel_buffer, src base.io_reader, workbuf slice base.u8) {
-	var block_size      base.u64[..255]
+	var block_size      base.u64[..= 255]
 	var need_block_size base.bool
 	var n_compressed    base.u64
 	var compressed      slice base.u8
@@ -1099,7 +1099,7 @@
 	var width_in_bytes  base.u64
 	var n               base.u64
 	var src_ri          base.u64
-	var bytes_per_pixel base.u32[..64]
+	var bytes_per_pixel base.u32[..= 64]
 	var pixfmt_channels base.u32
 	var tab             table base.u8
 	var i               base.u64
diff --git a/std/lzw/decode_lzw.wuffs b/std/lzw/decode_lzw.wuffs
index 5f02e99..9bb4342 100644
--- a/std/lzw/decode_lzw.wuffs
+++ b/std/lzw/decode_lzw.wuffs
@@ -31,21 +31,21 @@
 	// set_literal_width. This is assigned to the literal_width field at the
 	// start of decode_io_writer. During that method, calling set_literal_width
 	// will change set_literal_width_arg but not literal_width.
-	set_literal_width_arg base.u32[..9],
+	set_literal_width_arg base.u32[..= 9],
 
 	// read_from state that does not change during a decode call.
-	literal_width base.u32[..8],
-	clear_code    base.u32[..256],
-	end_code      base.u32[..257],
+	literal_width base.u32[..= 8],
+	clear_code    base.u32[..= 256],
+	end_code      base.u32[..= 257],
 
 	// read_from state that does change during a decode call.
-	save_code base.u32[..4096],
-	prev_code base.u32[..4095],
-	width     base.u32[..12],
+	save_code base.u32[..= 4096],
+	prev_code base.u32[..= 4095],
+	width     base.u32[..= 12],
 	bits      base.u32,
-	n_bits    base.u32[..31],
-	output_ri base.u32[..8191],
-	output_wi base.u32[..8191],
+	n_bits    base.u32[..= 31],
+	output_ri base.u32[..= 8191],
+	output_wi base.u32[..= 8191],
 
 	// read_from return value. The read_from method effectively returns a
 	// base.u32 to show how decode should continue after calling write_to. That
@@ -54,7 +54,7 @@
 	read_from_return_value base.u32,
 
 	// read_from per-code state.
-	prefixes array[4096] base.u16[..4095],
+	prefixes array[4096] base.u16[..= 4095],
 
 	util base.utility,
 )(
@@ -69,7 +69,7 @@
 	output array[8192 + 7] base.u8,
 )
 
-pub func decoder.set_literal_width!(lw base.u32[..8]) {
+pub func decoder.set_literal_width!(lw base.u32[..= 8]) {
 	this.set_literal_width_arg = args.lw + 1
 }
 
@@ -78,7 +78,7 @@
 }
 
 pub func decoder.decode_io_writer?(dst base.io_writer, src base.io_reader, workbuf slice base.u8) {
-	var i base.u32[..8191]
+	var i base.u32[..= 8191]
 
 	// Initialize read_from state.
 	this.literal_width = 8
@@ -124,23 +124,23 @@
 }
 
 pri func decoder.read_from!(src base.io_reader) {
-	var clear_code base.u32[..256]
-	var end_code   base.u32[..257]
+	var clear_code base.u32[..= 256]
+	var end_code   base.u32[..= 257]
 
-	var save_code base.u32[..4096]
-	var prev_code base.u32[..4095]
-	var width     base.u32[..12]
+	var save_code base.u32[..= 4096]
+	var prev_code base.u32[..= 4095]
+	var width     base.u32[..= 12]
 	var bits      base.u32
-	var n_bits    base.u32[..31]
-	var output_wi base.u32[..8191]
+	var n_bits    base.u32[..= 31]
+	var output_wi base.u32[..= 8191]
 
-	var code       base.u32[..4095]
-	var c          base.u32[..4095]
-	var o          base.u32[..8191]
+	var code       base.u32[..= 4095]
+	var c          base.u32[..= 4095]
+	var o          base.u32[..= 8191]
 	var steps      base.u32
 	var first_byte base.u8
-	var lm1_b      base.u16[..4095]
-	var lm1_a      base.u16[..4095]
+	var lm1_b      base.u16[..= 4095]
+	var lm1_a      base.u16[..= 4095]
 
 	clear_code = this.clear_code
 	end_code = this.end_code