Add single-quoted strings
diff --git a/doc/changelog.md b/doc/changelog.md
index c6190d6..b6a3061 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -16,6 +16,7 @@
 - Added `WUFFS_BASE__PIXEL_FORMAT__BGR_565`.
 - Added interfaces.
 - Added preprocessor.
+- Added single-quoted strings.
 - Added tokens.
 - Changed `gif.decoder_workbuf_len_max_incl_worst_case` from 1 to 0.
 - Made `wuffs_base__pixel_format` a struct.
diff --git a/doc/wuffs-the-language.md b/doc/wuffs-the-language.md
index 54c7cb3..1c081b4 100644
--- a/doc/wuffs-the-language.md
+++ b/doc/wuffs-the-language.md
@@ -22,9 +22,6 @@
 - [Iterate loops](/doc/note/iterate-loops.md).
 - Public vs private API is marked with the `pub` and `pri` keywords. Visibility
   boundaries are at the package level, unlike C++ or Java's type level.
-- No string type, only [slices](/doc/note/slices-arrays-and-tables.md) of
-  `base.u8`. Literals like `"#bad checksum"` are actually
-  [statuses](/doc/note/statuses.md).
 - No variable shadowing. All local variables must be declared before any other
   statements in a function body.
 - Like Go, semi-colons can be omitted. Similarly, the `()` parentheses around
@@ -88,6 +85,34 @@
 The `as` operator, e.g. `x as T`, converts an expression `x` to the type `T`.
 
 
+## Strings
+
+There is no string type. There are [arrays and
+slices](/doc/note/slices-arrays-and-tables.md) of bytes (`base.u8`s), but bear
+in mind that Wuffs code cannot [allocate or free
+memory](/doc/note/memory-safety.md).
+
+Double-quoted literals like `"#bad checksum"` are actually
+[statuses](/doc/note/statuses.md), [axiom names](/doc/note/assertions.md) or a
+`use "std/foo"` package name.
+
+Single-quoted literals are actually numerical constants. For example, `'A'` and
+`'\t'` are equivalent to `0x41` and `0x09`. These literals are not restricted
+to a single ASCII byte or even a single Unicode code point, and can decode to
+multiple bytes when finished with a `be` or `le` endianness suffix. For
+example, `'\x01\x02'be` is equivalent to `0x0102`. Similarly, `'\u0394?'le`
+(which can also be written `'Δ?'le`) is equivalent to `0x3F94CE`, because the
+UTF-8 encodings of U+0394 GREEK CAPITAL LETTER DELTA and U+003F QUESTION MARK
+(the ASCII `?`) is `(0xCE, 0x94)` and `(0x3F)`.
+
+Double-quoted literals cannot contain backslashes, as they'd be an unnecessary
+complication. Single-quoted literals can contain backslash-escapes, as they are
+often compared with arbitrary binary data. For example, where other programming
+languages would check if JPEG data starts with the magic _string_ `"\xFF\xD8"`,
+Wuffs would check if its opening 2 bytes, read as a little-endian `base.u16`,
+is a _number_ that equals `'\xFF\xD8'le`.
+
+
 ## Introductory Example
 
 A simple Wuffs the Language program, unrelated to Wuffs the Library, is
diff --git a/internal/cgen/expr.go b/internal/cgen/expr.go
index 7c2874d..3dd3898 100644
--- a/internal/cgen/expr.go
+++ b/internal/cgen/expr.go
@@ -70,7 +70,7 @@
 				b.writes("false")
 			}
 
-		} else if ident.IsStrLiteral(g.tm) {
+		} else if ident.IsDQStrLiteral(g.tm) {
 			if z := g.statusMap[n.StatusQID()]; z.cName != "" {
 				b.writes("wuffs_base__make_status(")
 				b.writes(z.cName)
diff --git a/internal/cgen/statement.go b/internal/cgen/statement.go
index b141b64..fd19ad3 100644
--- a/internal/cgen/statement.go
+++ b/internal/cgen/statement.go
@@ -441,7 +441,7 @@
 			b.writes("wuffs_base__make_status(NULL)")
 			isComplete = true
 		} else {
-			if retExpr.Ident().IsStrLiteral(g.tm) {
+			if retExpr.Ident().IsDQStrLiteral(g.tm) {
 				msg, _ := t.Unescape(retExpr.Ident().Str(g.tm))
 				isComplete = statusMsgIsNote(msg)
 			}
diff --git a/lang/ast/ast.go b/lang/ast/ast.go
index 7779f6e..d31ed6a 100644
--- a/lang/ast/ast.go
+++ b/lang/ast/ast.go
@@ -379,7 +379,7 @@
 
 // Assert is "assert RHS via ID2(args)", "pre etc", "inv etc" or "post etc":
 //  - ID0:   <IDAssert|IDPre|IDInv|IDPost>
-//  - ID2:   <string literal> reason
+//  - ID2:   <"-string literal> reason
 //  - RHS:   <Expr>
 //  - List0: <Arg> reason arguments
 type Assert Node
@@ -948,7 +948,7 @@
 }
 
 // Use is "use ID2":
-//  - ID2:   <string literal> package path
+//  - ID2:   <"-string literal> package path
 type Use Node
 
 func (n *Use) AsNode() *Node    { return (*Node)(n) }
diff --git a/lang/check/type.go b/lang/check/type.go
index 50da53a..7a0da26 100644
--- a/lang/check/type.go
+++ b/lang/check/type.go
@@ -364,7 +364,28 @@
 			n.SetMType(typeExprIdeal)
 			return nil
 
-		} else if id1.IsStrLiteral(q.tm) {
+		} else if id1.IsSQStrLiteral(q.tm) {
+			s := id1.Str(q.tm)
+			unescaped, ok := t.Unescape(id1.Str(q.tm))
+			if !ok {
+				return fmt.Errorf("check: invalid '-string literal %q", s)
+			}
+
+			z := big.NewInt(0)
+			i, iEnd, iDelta := 0, len(unescaped), +1 // Big-endian.
+			if (len(s) > 2) && (s[len(s)-2] == 'l') {
+				i, iEnd, iDelta = len(unescaped)-1, -1, -1 // Little-endian.
+			}
+			for ; i != iEnd; i += iDelta {
+				z.Lsh(z, 8)
+				z.Or(z, big.NewInt(int64(unescaped[i])))
+			}
+
+			n.SetConstValue(z)
+			n.SetMType(typeExprIdeal)
+			return nil
+
+		} else if id1.IsDQStrLiteral(q.tm) {
 			if _, ok := q.c.statuses[n.StatusQID()]; !ok {
 				return fmt.Errorf("check: unrecognized status %s", n.StatusQID().Str(q.tm))
 			}
diff --git a/lang/parse/parse.go b/lang/parse/parse.go
index ab54a69..93a3eea 100644
--- a/lang/parse/parse.go
+++ b/lang/parse/parse.go
@@ -113,9 +113,9 @@
 	case t.IDUse:
 		p.src = p.src[1:]
 		path := p.peek1()
-		if !path.IsStrLiteral(p.tm) {
+		if !path.IsDQStrLiteral(p.tm) {
 			got := p.tm.ByID(path)
-			return nil, fmt.Errorf(`parse: expected string literal, got %q at %s:%d`, got, p.filename, p.line())
+			return nil, fmt.Errorf(`parse: expected "-string literal, got %q at %s:%d`, got, p.filename, p.line())
 		}
 		p.src = p.src[1:]
 		if x := p.peek1(); x != t.IDSemicolon {
@@ -224,9 +224,9 @@
 			p.src = p.src[1:]
 
 			message := p.peek1()
-			if !message.IsStrLiteral(p.tm) {
+			if !message.IsDQStrLiteral(p.tm) {
 				got := p.tm.ByID(message)
-				return nil, fmt.Errorf(`parse: expected string literal, got %q at %s:%d`, got, p.filename, p.line())
+				return nil, fmt.Errorf(`parse: expected "-string literal, got %q at %s:%d`, got, p.filename, p.line())
 			}
 			if s, _ := t.Unescape(p.tm.ByID(message)); !isStatusMessage(s) {
 				return nil, fmt.Errorf(`parse: status message %q does not start with `+
@@ -604,9 +604,9 @@
 		if p.peek1() == t.IDVia {
 			p.src = p.src[1:]
 			reason = p.peek1()
-			if !reason.IsStrLiteral(p.tm) {
+			if !reason.IsDQStrLiteral(p.tm) {
 				got := p.tm.ByID(reason)
-				return nil, fmt.Errorf(`parse: expected string literal, got %q at %s:%d`, got, p.filename, p.line())
+				return nil, fmt.Errorf(`parse: expected "-string literal, got %q at %s:%d`, got, p.filename, p.line())
 			}
 			p.src = p.src[1:]
 			args, err = p.parseList(t.IDCloseParen, (*parser).parseArgNode)
@@ -1254,11 +1254,11 @@
 			p.src = p.src[1:]
 
 			if x := p.peek1(); x.IsLiteral(p.tm) {
-				if x.IsNumLiteral(p.tm) {
-					return nil, fmt.Errorf(`parse: dot followed by numeric literal at %s:%d`, p.filename, p.line())
+				if !x.IsDQStrLiteral(p.tm) {
+					return nil, fmt.Errorf(`parse: dot followed by non-"-string literal at %s:%d`, p.filename, p.line())
 				}
 				if !first {
-					return nil, fmt.Errorf(`parse: string literal %s has too many package qualifiers at %s:%d`,
+					return nil, fmt.Errorf(`parse: "-string literal %s has too many package qualifiers at %s:%d`,
 						x.Str(p.tm), p.filename, p.line())
 				}
 				p.src = p.src[1:]
diff --git a/lang/render/render.go b/lang/render/render.go
index a42ec18..2efa21b 100644
--- a/lang/render/render.go
+++ b/lang/render/render.go
@@ -291,7 +291,8 @@
 }
 
 func isCloseIdentStrLiteralQuestion(tm *t.Map, x t.ID) bool {
-	return x.IsClose() || x.IsIdent(tm) || x.IsStrLiteral(tm) || (x == t.IDQuestion)
+	return x.IsClose() || x.IsIdent(tm) || x.IsDQStrLiteral(tm) ||
+		x.IsSQStrLiteral(tm) || (x == t.IDQuestion)
 }
 
 func measureVarNameLength(tm *t.Map, lineTokens []t.Token, remaining []t.Token) uint32 {
diff --git a/lang/token/list.go b/lang/token/list.go
index 94e7767..d585c60 100644
--- a/lang/token/list.go
+++ b/lang/token/list.go
@@ -81,7 +81,8 @@
 	return false
 }
 
-func (x ID) IsStrLiteral(m *Map) bool {
+// IsDQStrLiteral returns whether x is a double-quote string literal.
+func (x ID) IsDQStrLiteral(m *Map) bool {
 	if x < nBuiltInIDs {
 		return false
 	} else if s := m.ByID(x); s != "" {
@@ -90,6 +91,16 @@
 	return false
 }
 
+// IsSQStrLiteral returns whether x is a single-quote string literal.
+func (x ID) IsSQStrLiteral(m *Map) bool {
+	if x < nBuiltInIDs {
+		return false
+	} else if s := m.ByID(x); s != "" {
+		return s[0] == '\''
+	}
+	return false
+}
+
 func (x ID) IsIdent(m *Map) bool {
 	if x < nBuiltInIDs {
 		return minBuiltInIdent <= x && x <= maxBuiltInIdent
diff --git a/lang/token/token.go b/lang/token/token.go
index b46e4dc..0d821e9 100644
--- a/lang/token/token.go
+++ b/lang/token/token.go
@@ -17,6 +17,7 @@
 import (
 	"errors"
 	"fmt"
+	"unicode/utf8"
 )
 
 const (
@@ -25,11 +26,115 @@
 	maxTokenSize = 1023
 )
 
+var backslashes = [256]byte{
+	'"':  0x22 | 0x80,
+	'\'': 0x27 | 0x80,
+	'/':  0x2F | 0x80,
+	'0':  0x00 | 0x80,
+	'?':  0x3F | 0x80,
+	'\\': 0x5C | 0x80,
+	'a':  0x07 | 0x80,
+	'b':  0x08 | 0x80,
+	'e':  0x1B | 0x80,
+	'f':  0x0C | 0x80,
+	'n':  0x0A | 0x80,
+	'r':  0x0D | 0x80,
+	't':  0x09 | 0x80,
+	'v':  0x0B | 0x80,
+}
+
 func Unescape(s string) (unescaped string, ok bool) {
-	if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
+	if len(s) < 2 {
 		return "", false
 	}
-	return s[1 : len(s)-1], true
+	switch s[0] {
+	case '"':
+		if s[len(s)-1] == '"' {
+			s = s[1 : len(s)-1]
+		} else {
+			return "", false
+		}
+	case '\'':
+		if s[len(s)-1] == '\'' {
+			s = s[1 : len(s)-1]
+		} else if (len(s) >= 4) && (s[len(s)-3] == '\'') &&
+			((s[len(s)-2] == 'b') || (s[len(s)-2] == 'l')) &&
+			(s[len(s)-1] == 'e') { // "be" or "le" suffix.
+			s = s[1 : len(s)-3]
+		} else {
+			return "", false
+		}
+	default:
+		return "", false
+	}
+
+	for i := 0; ; i++ {
+		if i == len(s) {
+			// There were no backslashes.
+			return s, true
+		}
+		if s[i] == '\\' {
+			break
+		}
+	}
+
+	// There were backslashes.
+	b := make([]byte, 0, len(s))
+	for i := 0; i < len(s); {
+		if s[i] != '\\' {
+			b = append(b, s[i])
+			i += 1
+			continue
+		} else if i >= (len(s) - 1) {
+			// No-op.
+		} else if x := backslashes[s[i+1]]; x != 0 {
+			b = append(b, x&0x7F)
+			i += 2
+			continue
+		} else if (s[i+1] == 'x') && (i < (len(s) - 3)) {
+			u0 := unhex(s[i+2])
+			u1 := unhex(s[i+3])
+			u := (u0 << 4) | u1
+			if 0 <= u {
+				b = append(b, uint8(u))
+				i += 4
+				continue
+			}
+		} else if (s[i+1] == 'u') && (i < (len(s) - 5)) {
+			u0 := unhex(s[i+2])
+			u1 := unhex(s[i+3])
+			u2 := unhex(s[i+4])
+			u3 := unhex(s[i+5])
+			u := (u0 << 12) | (u1 << 8) | (u2 << 4) | u3
+			if (u >= 0) && utf8.ValidRune(u) {
+				e := [utf8.UTFMax]byte{}
+				n := utf8.EncodeRune(e[:], u)
+				b = append(b, e[:n]...)
+				i += 6
+				continue
+			}
+		} else if (s[i+1] == 'U') && (i < (len(s) - 9)) {
+			u0 := unhex(s[i+2])
+			u1 := unhex(s[i+3])
+			u2 := unhex(s[i+4])
+			u3 := unhex(s[i+5])
+			u4 := unhex(s[i+6])
+			u5 := unhex(s[i+7])
+			u6 := unhex(s[i+8])
+			u7 := unhex(s[i+9])
+			u := (u0 << 28) | (u1 << 24) | (u2 << 20) | (u3 << 16) |
+				(u4 << 12) | (u5 << 8) | (u6 << 4) | u7
+			if (u >= 0) && utf8.ValidRune(u) {
+				e := [utf8.UTFMax]byte{}
+				n := utf8.EncodeRune(e[:], u)
+				b = append(b, e[:n]...)
+				i += 10
+				continue
+			}
+		}
+		return "", false
+	}
+	return string(b), true
 }
 
 type Map struct {
@@ -81,6 +186,18 @@
 	return ""
 }
 
+func unhex(c byte) int32 {
+	switch {
+	case 'A' <= c && c <= 'F':
+		return int32(c) - ('A' - 10)
+	case 'a' <= c && c <= 'f':
+		return int32(c) - ('a' - 10)
+	case '0' <= c && c <= '9':
+		return int32(c) - '0'
+	}
+	return -1
+}
+
 func alpha(c byte) bool {
 	return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || (c == '_')
 }
@@ -150,32 +267,45 @@
 			continue
 		}
 
-		// TODO: recognize escapes such as `\t`, `\"` and `\\`. For now, we
-		// assume that strings don't contain control bytes or backslashes.
-		// Neither should be necessary to parse `use "foo/bar"` lines.
-		if c == '"' {
+		if (c == '"') || (c == '\'') {
+			quote := c
 			j := i + 1
-			for ; j < len(src); j++ {
+			for j < len(src) {
 				c = src[j]
-				if c == '"' {
-					j++
+				j++
+				if c == quote {
 					break
-				}
-				if c == '\\' {
-					return nil, nil, fmt.Errorf("token: backslash in string at %s:%d", filename, line)
-				}
-				if c == '\n' {
-					return nil, nil, fmt.Errorf("token: expected final '\"' in string at %s:%d", filename, line)
-				}
-				if c < ' ' {
+				} else if c == '\\' {
+					if quote == '"' {
+						return nil, nil, fmt.Errorf("token: backslash in \"-string at %s:%d", filename, line)
+					}
+				} else if c == '\n' {
+					return nil, nil, fmt.Errorf("token: expected final %c in string at %s:%d", quote, filename, line)
+				} else if c < ' ' {
 					return nil, nil, fmt.Errorf("token: control character in string at %s:%d", filename, line)
 				}
-				// The -1 is because we still haven't seen the final '"'.
-				if j-i == maxTokenSize-1 {
-					return nil, nil, fmt.Errorf("token: string too long at %s:%d", filename, line)
+			}
+
+			hasEndian := (quote == '\'') && (j < (len(src) - 2)) &&
+				((src[j] == 'b') || (src[j] == 'l')) &&
+				(src[j+1] == 'e')
+			if hasEndian {
+				j += 2
+			}
+
+			if j-i > maxTokenSize {
+				return nil, nil, fmt.Errorf("token: string too long at %s:%d", filename, line)
+			}
+			s := string(src[i:j])
+			if quote == '\'' {
+				if unescaped, ok := Unescape(s); !ok {
+					return nil, nil, fmt.Errorf("token: invalid '-string at %s:%d", filename, line)
+				} else if (len(unescaped) > 1) && !hasEndian {
+					return nil, nil, fmt.Errorf("token: multi-byte '-string needs be or le suffix at %s:%d", filename, line)
 				}
 			}
-			id, err := m.Insert(string(src[i:j]))
+
+			id, err := m.Insert(s)
 			if err != nil {
 				return nil, nil, err
 			}
diff --git a/std/bmp/decode_bmp.wuffs b/std/bmp/decode_bmp.wuffs
index 032f798..5cbe82a 100644
--- a/std/bmp/decode_bmp.wuffs
+++ b/std/bmp/decode_bmp.wuffs
@@ -70,7 +70,7 @@
 	// Read the BITMAPFILEHEADER (14 bytes).
 
 	magic = args.src.read_u16le_as_u32?()
-	if magic <> 0x4D42 {  // "BM" little-endian.
+	if magic <> 'BM'le {
 		return "#bad header"
 	}
 
diff --git a/std/gif/decode_config.wuffs b/std/gif/decode_config.wuffs
index 235bdf8..67d8f5b 100644
--- a/std/gif/decode_config.wuffs
+++ b/std/gif/decode_config.wuffs
@@ -180,9 +180,9 @@
 }
 
 pub func config_decoder.set_report_metadata!(fourcc: base.u32, report: base.bool) {
-	if args.fourcc == 0x4943_4350 {  // "ICCP"
+	if args.fourcc == 'ICCP'be {
 		this.report_metadata_iccp = args.report
-	} else if args.fourcc == 0x584D_5020 {  // "XMP "
+	} else if args.fourcc == 'XMP 'be {
 		this.report_metadata_xmp = args.report
 	}
 }
@@ -204,7 +204,7 @@
 
 		this.metadata_chunk_length_value = args.src.peek_u8_as_u64()
 		if this.metadata_chunk_length_value > 0 {
-			if this.metadata_fourcc_value == 0x584D_5020 {  // "XMP "
+			if this.metadata_fourcc_value == 'XMP 'be {
 				// The +1 is because XMP metadata's encoding includes each
 				// block's leading byte (the block size) as part of the
 				// metadata passed to the caller.
@@ -427,8 +427,8 @@
 		c[i] = args.src.read_u8?()
 		i += 1
 	}
-	if (c[0] <> 0x47) or (c[1] <> 0x49) or (c[2] <> 0x46) or (c[3] <> 0x38) or
-		((c[4] <> 0x37) and (c[4] <> 0x39)) or (c[5] <> 0x61) {
+	if (c[0] <> 'G') or (c[1] <> 'I') or (c[2] <> 'F') or (c[3] <> '8') or
+		((c[4] <> '7') and (c[4] <> '9')) or (c[5] <> 'a') {
 		return "#bad header"
 	}
 }
@@ -617,7 +617,7 @@
 			}
 			this.metadata_chunk_length_value = args.src.peek_u8_as_u64()
 			args.src.skip32_fast!(actual: 1, worst_case: 1)
-			this.metadata_fourcc_value = 0x4943_4350  // "ICCP"
+			this.metadata_fourcc_value = 'ICCP'be
 			this.metadata_io_position = args.src.position() ~sat+ this.metadata_chunk_length_value
 			this.call_sequence = 1
 			return base."@metadata reported"
@@ -637,7 +637,7 @@
 			} else {
 				args.src.skip32_fast!(actual: 1, worst_case: 1)
 			}
-			this.metadata_fourcc_value = 0x584D_5020  // "XMP "
+			this.metadata_fourcc_value = 'XMP 'be
 			this.metadata_io_position = args.src.position() ~sat+ this.metadata_chunk_length_value
 			this.call_sequence = 1
 			return base."@metadata reported"
diff --git a/std/gif/decode_gif.wuffs b/std/gif/decode_gif.wuffs
index 4f98771..56c23da 100644
--- a/std/gif/decode_gif.wuffs
+++ b/std/gif/decode_gif.wuffs
@@ -219,9 +219,9 @@
 }
 
 pub func decoder.set_report_metadata!(fourcc: base.u32, report: base.bool) {
-	if args.fourcc == 0x4943_4350 {  // "ICCP"
+	if args.fourcc == 'ICCP'be {
 		this.report_metadata_iccp = args.report
-	} else if args.fourcc == 0x584D_5020 {  // "XMP "
+	} else if args.fourcc == 'XMP 'be {
 		this.report_metadata_xmp = args.report
 	}
 }
@@ -243,7 +243,7 @@
 
 		this.metadata_chunk_length_value = args.src.peek_u8_as_u64()
 		if this.metadata_chunk_length_value > 0 {
-			if this.metadata_fourcc_value == 0x584D_5020 {  // "XMP "
+			if this.metadata_fourcc_value == 'XMP 'be {
 				// The +1 is because XMP metadata's encoding includes each
 				// block's leading byte (the block size) as part of the
 				// metadata passed to the caller.
@@ -502,8 +502,8 @@
 		c[i] = args.src.read_u8?()
 		i += 1
 	}
-	if (c[0] <> 0x47) or (c[1] <> 0x49) or (c[2] <> 0x46) or (c[3] <> 0x38) or
-		((c[4] <> 0x37) and (c[4] <> 0x39)) or (c[5] <> 0x61) {
+	if (c[0] <> 'G') or (c[1] <> 'I') or (c[2] <> 'F') or (c[3] <> '8') or
+		((c[4] <> '7') and (c[4] <> '9')) or (c[5] <> 'a') {
 		return "#bad header"
 	}
 }
@@ -692,7 +692,7 @@
 			}
 			this.metadata_chunk_length_value = args.src.peek_u8_as_u64()
 			args.src.skip32_fast!(actual: 1, worst_case: 1)
-			this.metadata_fourcc_value = 0x4943_4350  // "ICCP"
+			this.metadata_fourcc_value = 'ICCP'be
 			this.metadata_io_position = args.src.position() ~sat+ this.metadata_chunk_length_value
 			this.call_sequence = 1
 			return base."@metadata reported"
@@ -712,7 +712,7 @@
 			} else {
 				args.src.skip32_fast!(actual: 1, worst_case: 1)
 			}
-			this.metadata_fourcc_value = 0x584D_5020  // "XMP "
+			this.metadata_fourcc_value = 'XMP 'be
 			this.metadata_io_position = args.src.position() ~sat+ this.metadata_chunk_length_value
 			this.call_sequence = 1
 			return base."@metadata reported"
diff --git a/std/json/decode_json.wuffs b/std/json/decode_json.wuffs
index 7748504..9062909 100644
--- a/std/json/decode_json.wuffs
+++ b/std/json/decode_json.wuffs
@@ -361,7 +361,7 @@
 									continue.string_loop_outer
 								}
 
-							} else if c == 0x75 {  // 0x75 is 'u'.
+							} else if c == 'u' {
 								// -------- BEGIN backslash-u.
 								if args.src.available() < 6 {
 									if args.src.is_closed() {
@@ -437,8 +437,8 @@
 									uni4_string = args.src.peek_u64le_at(offset: 4) >> 16
 
 									// Look for the low surrogate's "\\u".
-									if ((0xFF & (uni4_string >> 0)) <> 0x5C) or
-										((0xFF & (uni4_string >> 8)) <> 0x75) {
+									if ((0xFF & (uni4_string >> 0)) <> '\\') or
+										((0xFF & (uni4_string >> 8)) <> 'u') {
 										uni4_high_surrogate = 0
 										uni4_value = 0
 										uni4_ok = 0
@@ -492,8 +492,8 @@
 								}
 								// -------- END   backslash-u.
 
-							} else if (c == 0x55) and
-								this.quirk_enabled_allow_backslash_capital_u {  // 0x55 is 'U'.
+							} else if (c == 'U') and
+								this.quirk_enabled_allow_backslash_capital_u {
 								// -------- BEGIN backslash-capital-u.
 								if args.src.available() < 10 {
 									if args.src.is_closed() {
@@ -559,8 +559,8 @@
 								}
 								// -------- END   backslash-capital-u.
 
-							} else if (c == 0x78) and
-								this.quirk_enabled_allow_backslash_x {  // 0x78 is 'x'.
+							} else if (c == 'x') and
+								this.quirk_enabled_allow_backslash_x {
 								// -------- BEGIN backslash-x
 								if args.src.available() < 4 {
 									if args.src.is_closed() {
@@ -1030,7 +1030,7 @@
 				continue.outer
 
 			} else if class == 0x09 {  // 0x09 is CLASS_FALSE.
-				match = args.src.match7(a: 0x6573_6C61_6605)  // 5 bytes "false".
+				match = args.src.match7(a: '\x05false'le)
 				if match == 0 {
 					args.dst.write_fast_token!(
 						value_major: 0,
@@ -1048,7 +1048,7 @@
 				}
 
 			} else if class == 0x0A {  // 0x0A is CLASS_TRUE.
-				match = args.src.match7(a: 0x65_7572_7404)  // 4 bytes "true".
+				match = args.src.match7(a: '\x04true'le)
 				if match == 0 {
 					args.dst.write_fast_token!(
 						value_major: 0,
@@ -1066,7 +1066,7 @@
 				}
 
 			} else if class == 0x0B {  // 0x0B is CLASS_NULL_NAN_INF.
-				match = args.src.match7(a: 0x6C_6C75_6E04)  // 4 bytes "null".
+				match = args.src.match7(a: '\x04null'le)
 				if match == 0 {
 					args.dst.write_fast_token!(
 						value_major: 0,
@@ -1131,7 +1131,7 @@
 		c = args.src.peek_u8()
 
 		// Scan the optional minus sign.
-		if c <> 0x2D {  // 0x2D is '-'.
+		if c <> '-' {
 			assert args.src.available() > 0
 			assert n <= 1
 		} else {
@@ -1153,7 +1153,7 @@
 		}
 
 		// Scan the opening digits.
-		if c == 0x30 {  // 0x30 is '0'.
+		if c == '0' {
 			n += 1
 			args.src.skip32_fast!(actual: 1, worst_case: 1)
 			assert n <= 99
@@ -1175,7 +1175,7 @@
 		c = args.src.peek_u8()
 
 		// Scan the optional fraction.
-		if c <> 0x2E {  // 0x2E is '.'.
+		if c <> '.' {
 			assert args.src.available() > 0
 			assert n <= 99
 		} else {
@@ -1206,7 +1206,7 @@
 		}
 
 		// Scan the optional 'E' or 'e'.
-		if (c <> 0x45) and (c <> 0x65) {  // 0x45 and 0x65 are 'E' and 'e'.
+		if (c <> 'E') and (c <> 'e') {
 			break.goto_done
 		}
 		if n >= 99 {
@@ -1229,7 +1229,7 @@
 		c = args.src.peek_u8()
 
 		// Scan the optional '+' or '-'.
-		if (c <> 0x2B) and (c <> 0x2D) {  // 0x2B and 0x2D are '+' and '-'.
+		if (c <> '+') and (c <> '-') {
 			assert n <= 99
 		} else {
 			if n >= 99 {
@@ -1354,7 +1354,7 @@
 	}
 	c2 = args.src.peek_u16le()
 
-	if (c2 == 0x2A2F) and this.quirk_enabled_allow_comment_block {
+	if (c2 == '/*'le) and this.quirk_enabled_allow_comment_block {
 		args.src.skip32_fast!(actual: 2, worst_case: 2)
 		length = 2
 
@@ -1386,7 +1386,7 @@
 				}
 
 				c2 = args.src.peek_u16le()
-				if c2 == 0x2F2A {  // 0x2F2A is "*/" little-endian.
+				if c2 == '*/'le {
 					args.src.skip32_fast!(actual: 2, worst_case: 2)
 					args.dst.write_fast_token!(
 						value_major: 0,
@@ -1411,7 +1411,7 @@
 			}
 		} endwhile.comment_block
 
-	} else if (c2 == 0x2F2F) and this.quirk_enabled_allow_comment_line {
+	} else if (c2 == '//'le) and this.quirk_enabled_allow_comment_line {
 		args.src.skip32_fast!(actual: 2, worst_case: 2)
 		length = 2
 
@@ -1443,7 +1443,7 @@
 				}
 
 				c = args.src.peek_u8()
-				if c == 0x0A {  // 0x0A is '\n'.
+				if c == '\n' {
 					args.src.skip32_fast!(actual: 1, worst_case: 1)
 					args.dst.write_fast_token!(
 						value_major: 0,
@@ -1492,9 +1492,9 @@
 		// Bitwise or'ing with 0x20 converts upper case ASCII to lower case.
 
 		c4 = args.src.peek_u24le_as_u32()
-		if (c4 | 0x20_2020) == 0x66_6E69 {
+		if (c4 | 0x20_2020) == 'inf'le {
 			if args.src.available() > 7 {
-				if (args.src.peek_u64le() | 0x2020_2020_2020_2020) == 0x7974_696E_6966_6E69 {
+				if (args.src.peek_u64le() | 0x2020_2020_2020_2020) == 'infinity'le {
 					args.dst.write_fast_token!(
 						value_major: 0,
 						value_minor: 0xA0_0020,
@@ -1515,7 +1515,7 @@
 			args.src.skip32_fast!(actual: 3, worst_case: 3)
 			return ok
 
-		} else if (c4 | 0x20_2020) == 0x6E_616E {
+		} else if (c4 | 0x20_2020) == 'nan'le {
 			args.dst.write_fast_token!(
 				value_major: 0,
 				value_minor: 0xA0_0080,
@@ -1523,9 +1523,9 @@
 				length: 3)
 			args.src.skip32_fast!(actual: 3, worst_case: 3)
 			return ok
-		} else if (c4 & 0xFF) == 0x2B {  // 0x2B is '+'.
+		} else if (c4 & 0xFF) == '+' {
 			neg = 0
-		} else if (c4 & 0xFF) == 0x2D {  // 0x2D is '-'.
+		} else if (c4 & 0xFF) == '-' {
 			neg = 1
 		} else {
 			return "#bad input"
@@ -1540,9 +1540,9 @@
 		}
 
 		c4 = args.src.peek_u32le() >> 8
-		if (c4 | 0x20_2020) == 0x66_6E69 {
+		if (c4 | 0x20_2020) == 'inf'le {
 			if args.src.available() > 8 {
-				if (args.src.peek_u64le_at(offset: 1) | 0x2020_2020_2020_2020) == 0x7974_696E_6966_6E69 {
+				if (args.src.peek_u64le_at(offset: 1) | 0x2020_2020_2020_2020) == 'infinity'le {
 					args.dst.write_fast_token!(
 						value_major: 0,
 						value_minor: 0xA0_0000 | ((0x20 as base.u32) >> neg),
@@ -1563,7 +1563,7 @@
 			args.src.skip32_fast!(actual: 4, worst_case: 4)
 			return ok
 
-		} else if (c4 | 0x20_2020) == 0x6E_616E {
+		} else if (c4 | 0x20_2020) == 'nan'le {
 			args.dst.write_fast_token!(
 				value_major: 0,
 				value_minor: 0xA0_0000 | ((0x80 as base.u32) >> neg),
@@ -1616,11 +1616,11 @@
 			}
 
 			args.src.skip32_fast!(actual: 1, worst_case: 1)
-			if (whitespace_length >= 0xFFFE) or (c == 0x0A) {
+			if (whitespace_length >= 0xFFFE) or (c == '\n') {
 				args.dst.write_fast_token!(
 					value_major: 0, value_minor: 0, link: 0x0, length: whitespace_length + 1)
 				whitespace_length = 0
-				if c == 0x0A {
+				if c == '\n' {
 					break.outer
 				}
 				continue.outer