// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
pub impl Show for Unit with fn output(_self, logger) {
logger.write_string("()")
}
///|
pub impl Show for Unit with fn to_string(_self) {
"()"
}
///|
pub impl Show for Bool with fn to_string(self) {
if self {
"true"
} else {
"false"
}
}
///|
pub impl Show for Int with fn to_string(self) {
Int::to_string(self)
}
///|
pub impl Show for Int64 with fn to_string(self) {
Int64::to_string(self)
}
///|
pub impl Show for UInt with fn to_string(self) {
UInt::to_string(self)
}
///|
pub impl Show for UInt64 with fn to_string(self) {
UInt64::to_string(self)
}
///|
pub impl Show for Byte with fn to_string(self) {
Byte::to_string(self)
}
///|
pub impl Show for UInt16 with fn to_string(self) {
UInt16::to_string(self)
}
///|
/// Convert a byte to a two-digit lowercase hexadecimal string.
///
/// Example:
///
/// ```mbt check
/// test {
/// inspect(Byte::to_hex(b'\x0f'), content="0f")
/// }
/// ```
pub fn Byte::to_hex(b : Byte) -> String {
fn to_hex_digit(i : Byte) -> Char {
if i < 10 {
(i + b'0').to_char()
} else {
(i + b'a' - 10).to_char()
}
}
[to_hex_digit(b / 16), to_hex_digit(b % 16)]
}
///|
test "to_hex_digit" {
inspect(Byte::to_hex(b'\xee'), content="ee")
inspect(Byte::to_hex(b'\xf3'), content="f3")
}
///|
/// Returns the escaped representation of a string.
///
/// When `quote` is true (default), the result is wrapped in double quotes
/// like a MoonBit string literal.
///
/// Escape rules:
/// - Double quote and backslash are backslash-escaped: `\"`, `\\`
/// - Common control characters use named escapes: `\n`, `\r`, `\b`, `\t`
/// - Other control characters (< U+0020) use `\u{hex}` format
/// - All other characters are displayed as-is
///
/// ```mbt check
/// test {
/// inspect("Hello \n".escape(), content="\"Hello \\n\"")
/// inspect("Hello \n".escape(quote=false), content="Hello \\n")
/// }
/// ```
pub fn String::escape(self : String, quote? : Bool = true) -> String {
let buf = StringBuilder()
self[:].escape_to(buf, quote~)
buf.to_string()
}
///|
/// Returns the escaped representation of a string view.
///
/// When `quote` is true (default), the result is wrapped in double quotes
/// like a MoonBit string literal.
///
/// Escape rules:
/// - Double quote and backslash are backslash-escaped: `\"`, `\\`
/// - Common control characters use named escapes: `\n`, `\r`, `\b`, `\t`
/// - Other control characters (< U+0020) use `\u{hex}` format
/// - All other characters are displayed as-is
///
/// ```mbt check
/// test {
/// inspect("Hello\nWorld"[:6].escape(), content="\"Hello\\n\"")
/// inspect("Hello\nWorld"[:6].escape(quote=false), content="Hello\\n")
/// }
/// ```
pub fn StringView::escape(
self : StringView,
quote? : Bool = true,
) -> StringView {
let buf = StringBuilder()
self.escape_to(buf, quote~)
buf.to_string()
}
///|
// js and wasm-gc have no vector loads, so they keep the scalar loop.
#cfg(not(any(target="native", target="wasm")))
fn StringView::escape_to(
self : StringView,
logger : &Logger,
quote? : Bool = true,
) -> Unit {
if quote {
logger.write_char('"')
}
let len = self.length()
fn flush_segment(seg : Int, i : Int) {
if i > seg {
logger.write_view(self[seg:i])
}
}
for i = 0, seg = 0 {
if i >= len {
flush_segment(seg, i)
break
}
let code = self.unsafe_get(i)
match code {
'"' | '\\' as c => {
flush_segment(seg, i)
logger.write_char('\\')
logger.write_char(c.unsafe_to_char())
continue i + 1, i + 1
}
'\n' => {
flush_segment(seg, i)
logger.write_string("\\n")
continue i + 1, i + 1
}
'\r' => {
flush_segment(seg, i)
logger.write_string("\\r")
continue i + 1, i + 1
}
'\b' => {
flush_segment(seg, i)
logger.write_string("\\b")
continue i + 1, i + 1
}
'\t' => {
flush_segment(seg, i)
logger.write_string("\\t")
continue i + 1, i + 1
}
code =>
if code < ' ' {
flush_segment(seg, i)
logger <+ "\\u{\{cb => cb.write_string(code.to_byte().to_hex())}}"
continue i + 1, i + 1
} else {
continue i + 1, seg
}
}
}
if quote {
logger.write_char('"')
}
}
///|
// Same output and `Logger` call sequence as the scalar loop above, but the
// unescaped runs are found with `find_escape_code_unit`, eight UTF-16 code
// units at a time for longer runs.
//
// This relies on `String`/`StringView` being well-formed UTF-16 (see
// `StringView::make_view`). Every run boundary is either an end of the view or
// next to an escaped code unit, which is ASCII, so no boundary splits a
// surrogate pair: the scalar loop's `self[seg:i]` never snaps a boundary, and
// the unchecked `make_view` below yields the same well-formed run.
#cfg(any(target="native", target="wasm"))
fn StringView::escape_to(
self : StringView,
logger : &Logger,
quote? : Bool = true,
) -> Unit {
if quote {
logger.write_char('"')
}
let data = self.str()
let end = self.start() + self.length()
for i = self.start(), seg = self.start() {
let i = find_escape_code_unit(data, i, end)
if i > seg {
logger.write_view(StringView::make_view(data, seg, i))
}
guard i < end else { break }
let code = data.unsafe_get(i)
match code {
'"' | '\\' as c => {
logger.write_char('\\')
logger.write_char(c.unsafe_to_char())
}
'\n' => logger.write_string("\\n")
'\r' => logger.write_string("\\r")
'\b' => logger.write_string("\\b")
'\t' => logger.write_string("\\t")
code => logger <+ "\\u{\{cb => cb.write_string(code.to_byte().to_hex())}}"
}
continue i + 1, i + 1
}
if quote {
logger.write_char('"')
}
}
///|
// Returns the index of the first code unit in `data[pos:end]` that
// `String::escape` rewrites (a double quote, a backslash, or a control
// character below U+0020), or `end` when there is none. The caller must
// ensure `0 <= pos <= end <= data.length()`.
//
// Ranges shorter than two SIMD blocks, which include most short strings,
// are scanned one code unit at a time. Longer ones check `pos` first, so a
// run of consecutive escapes does not pay for a vector scan per escape.
#cfg(any(target="native", target="wasm"))
#inline
fn find_escape_code_unit(data : String, pos : Int, end : Int) -> Int {
if end - pos < 16 {
find_escape_code_unit_scalar(data, pos, end)
} else if code_unit_needs_escape(data.unsafe_get(pos)) {
pos
} else {
find_escape_code_unit_simd(data, pos + 1, end)
}
}
///|
#cfg(any(target="native", target="wasm"))
#inline
fn code_unit_needs_escape(code : UInt16) -> Bool {
code == '"' || code == '\\' || code < ' '
}
///|
#cfg(any(target="native", target="wasm"))
fn find_escape_code_unit_scalar(data : String, pos : Int, end : Int) -> Int {
for i in pos.. Int {
let quote = i16x8_splat('"')
let backslash = i16x8_splat('\\')
let control_max = i16x8_splat(0x1F)
// `end - p` cannot overflow, unlike `p + 8` for offsets near `Int` max.
let tail_start = for p = pos; end - p >= 8; {
let block = v128_load_i16x8(data, p)
let escaped = v128_or(
i16x8_le_u(block, control_max),
v128_or(i16x8_eq(block, quote), i16x8_eq(block, backslash)),
)
if v128_any_true(escaped) {
// Lane k sets bit k, so the lowest set bit is the first match.
return p + i16x8_bitmask(escaped).ctz()
}
continue p + 8
} nobreak {
p
}
find_escape_code_unit_scalar(data, tail_start, end)
}
///|
/// Returns the original string without escaping.
/// `Show::output` for `String` also writes the raw string through this
/// `Show::to_string` implementation.
/// Use `String::escape` when a quoted and escaped representation is needed.
/// # Examples
///
/// ```mbt check
/// test {
/// let str = "Hello \n"
/// inspect(str.to_string(), content="Hello \n")
/// inspect(str.escape(quote=true), content="\"Hello \\n\"")
/// }
/// ```
pub impl Show for String with fn to_string(self) {
self
}
///|
#deprecated("Use Debug instead of Show for debugging purposes. See https://github.com/moonbitlang/core/blob/main/debug/README.mbt.md")
pub impl[X : Show] Show for X?
///|
pub impl[X : Show] Show for X? with fn output(self, logger) {
match self {
None => logger.write_string("None")
Some(arg) =>
logger <+
$|Some(\{arg})
}
}
///|
#deprecated("Use Debug instead of Show for debugging purposes. See https://github.com/moonbitlang/core/blob/main/debug/README.mbt.md")
pub impl[T : Show, E : Show] Show for Result[T, E]
///|
pub impl[T : Show, E : Show] Show for Result[T, E] with fn output(self, logger) {
match self {
Ok(x) =>
logger <+
$|Ok(\{x})
Err(e) =>
logger <+
$|Err(\{e})
}
}
///|
#deprecated("Use Debug instead of Show for debugging purposes. See https://github.com/moonbitlang/core/blob/main/debug/README.mbt.md")
pub impl[X : Show] Show for FixedArray[X]
///|
pub impl[X : Show] Show for FixedArray[X] with fn output(self, logger) {
logger.write_iter(self.iter())
}
///|
#deprecated("Use Debug instead of Show for debugging purposes. See https://github.com/moonbitlang/core/blob/main/debug/README.mbt.md")
pub impl[X : Show] Show for Array[X]
///|
pub impl[X : Show] Show for Array[X] with fn output(self, logger) {
logger.write_iter(self.iter())
}