// 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.
///|
/// Default max depth for pruning.
let default_max_depth : Int = 16
///|
/// Default layout threshold for compact-vs-multiline decisions.
///
/// Larger values prefer single-line output.
let default_threshold : Int = 70
///|
/// Prune a `Repr` tree to a given depth (replacing pruned subtrees with
/// `Repr::omitted()`). Returns `self` unchanged when `max_depth` is `None`.
///
/// Parameters:
/// - `max_depth~`: optional maximum depth. `None` means no pruning.
/// Values `<= 0` are treated as `1`.
/// - `replacement?`: the node used to replace pruned subtrees.
/// Defaults to `Repr::omitted()`.
///
/// Semantics:
/// - Leaf nodes are never replaced.
/// - When the depth budget is exhausted and the node has children:
/// - if `info_adds_depth() == false`, pruning continues into its children;
/// - otherwise the whole subtree is replaced by `replacement`.
fn Repr::prune_info(
self : Repr,
replacement? : Repr = Repr::omitted(),
max_depth~ : Int?,
) -> Repr {
match max_depth {
None => self
Some(depth) => {
fn go(d : Int, node : Repr) -> Repr {
let children = node.children()
if d <= 0 {
if children.is_empty() {
node
} else if !node.info_adds_depth() {
node.with_children(children.map(child => go(d, child)))
} else {
replacement
}
} else if children.is_empty() {
node
} else {
let next_depth = if node.info_adds_depth() { d - 1 } else { d }
node.with_children(children.map(child => go(next_depth, child)))
}
}
go(Int::max(1, depth), self)
}
}
}
///|
/// Whether visiting this node should consume (decrement) the remaining depth budget.
///
/// This is used by depth-limited pruning: structural edge nodes like
/// `RecordField` / `EnumLabeledArg` / `MapEntry` do not count as a depth level,
/// so field names / labels can still be preserved when pruning.
fn Repr::info_adds_depth(info : Repr) -> Bool {
!(info is (RecordField(_, _) | EnumLabeledArg(_, _) | MapEntry(_, _)))
}
///|
/// Heuristic "size" of a node for compact-vs-multiline decisions.
fn info_size(info : Repr) -> Int {
match info {
UnitLit => 1
Integer(_) => 1
DoubleLit(_) => 1
FloatLit(_) => 1
BoolLit(_) => 1
CharLit(_) => 1
StringLit(s) => if s.length() <= 15 { 1 } else { 2 }
Tuple(_) => 1
Array(_) => 1
Record(_) => 2
RecordField(name, _) => if name.length() <= 15 { 0 } else { 1 }
EnumLabeledArg(name, _) => if name.length() <= 15 { 0 } else { 1 }
Enum(name, _) => if name.length() <= 15 { 1 } else { 2 }
Opaque(name, _) => if name.length() <= 15 { 1 } else { 2 }
Literal(s) => if s.length() <= 15 { 1 } else { 2 }
Map(_) => 2
MapEntry(_, _) => 0
Omitted => 0
}
}
///|
/// Returns `true` when a record field name can be printed without quotes.
fn is_unquoted_key(key : String) -> Bool {
key is ['a'..='z' | '_', .. rest] &&
(for rest = rest {
match rest {
['a'..='z' | 'A'..='Z' | '0'..='9' | '_', .. rest] => continue rest
[_, ..] => break false
[] => break true
}
})
}
///|
/// Pretty-print a record field name (quoted when needed).
fn pretty_print_label(name : String) -> String {
if is_unquoted_key(name) {
name
} else {
name.escape()
}
}
///|
/// Render a single `Repr` node (label + rendered children) into `Content`.
fn Repr::pretty_print(self : Repr, children : Array[Content]) -> Content {
match self {
UnitLit => comma_seq("(", ")", [])
Integer(s) =>
no_parens(content_parens(if s is ['-', ..] { 1 } else { 0 }, [s]))
DoubleLit(x) => {
let needs_parens = 1.0 / x < 0.0
leaf(x.to_string(), needs_parens~)
}
FloatLit(x) => {
let needs_parens : Bool = (1.0 : Float) / x < (0.0 : Float)
leaf(x.to_string(), needs_parens~)
}
BoolLit(x) => leaf(x.to_string())
CharLit(x) => leaf(x.escape())
StringLit(x) => no_parens(verbatim(x.escape()))
Tuple(_) => comma_seq("(", ")", children.map(x => x.no_wrap()))
Enum(name, _) =>
match children {
[] => no_parens(verbatim(name))
_ =>
if name == "Tuple" {
comma_seq("(", ")", children.map(x => x.no_wrap()))
} else {
comma_seq(name + "(", ")", children.map(x => x.no_wrap()))
}
}
Array(_) => comma_seq("[", "]", children.map(x => x.no_wrap()))
Record(_) => comma_seq("{", "}", children.map(x => x.no_wrap()))
Opaque(name, _) =>
if children.is_empty() {
no_parens(surround("<", ">", verbatim(name)))
} else {
let body = verbatim(name + ":") +
indent(
" ",
comma_seq("", "", children.map(x => x.no_wrap())).no_wrap(),
)
no_parens(surround("<", ">", body))
}
Literal(str) => no_parens(verbatim(str))
Map(_) => comma_seq("{", "}", children.map(x => x.no_wrap()))
MapEntry(_, _) =>
match children {
[key, val] => {
let k = key.no_wrap()
let v = val.no_wrap()
match v.lines {
[] => empty_content()
[one] =>
no_parens(
content_parens(1 + k.size + v.size, [
print_content(surround("", ": ", k)) + one,
]),
)
[first, .. rest] => {
let head = print_content(surround("", ": ", k)) + first
no_parens(content_parens(1 + k.size + v.size, [head, ..rest]))
}
}
}
_ => empty_content()
}
EnumLabeledArg(name, _) =>
match children {
[val] =>
match val.lines {
[] => empty_content()
[first] =>
no_parens(content_parens(1 + val.size, [name + "=" + first]))
[first, .. rest] =>
no_parens(
content_parens(1 + val.size, [name + "=" + first, ..rest]),
)
}
_ => empty_content()
}
RecordField(name, _) =>
match children {
[val] => {
let label = pretty_print_label(name)
let v = val.no_wrap()
match v.lines {
[] => empty_content()
[one] => no_parens(content_parens(1 + v.size, [label + ": " + one]))
[first, .. rest] =>
no_parens(
content_parens(1 + v.size, [label + ": " + first, ..rest]),
)
}
}
_ => empty_content()
}
Omitted => parens(verbatim("..."))
}
}
///|
/// Render a `Repr` as `Content` with resizing decisions.
fn Repr::render_repr(self : Repr, threshold : Int) -> Content {
let label = self.shallow()
let children = self.children().map(child => child.render_repr(threshold))
with_resizing(info_size(label), threshold, label.pretty_print(children))
}
///|
/// Pretty-print a `Repr`.
///
/// Optional parameters:
/// - `max_depth?`: maximum expansion depth; deeper subtrees are replaced with `...`.
/// Defaults to `4` when `None`. Values `<= 0` are treated as `1`.
/// - `compact_threshold?`: compact-vs-multiline layout threshold.
/// The printer uses a heuristic "size" for nodes; when the structure is deemed
/// small enough under this threshold, it is kept on one line, otherwise it is
/// broken into multiple lines.
/// Larger values prefer single-line output. Defaults to `80` when `None`.
pub fn render(r : Repr, max_depth? : Int) -> String {
let max_depth : Int? = match max_depth {
Some(_) => max_depth
None => Some(default_max_depth)
}
let threshold = default_threshold
let info = r.prune_info(max_depth~)
print_content(info.render_repr(threshold).no_wrap())
}
///|
pub impl Show for Repr with fn output(self, logger) {
logger.write_string(render(self))
}
///|
/// `Repr` is its own debug representation: `to_repr` is the identity.
pub impl Debug for Repr with fn to_repr(self) {
self
}
///|
/// Print a value in human-readable format to standard output.
pub fn[T : Debug] debug(x : T) -> Unit {
println(render(x.to_repr()))
}
///|
/// Convert a value to its debug string representation. Equivalent to
/// `Repr(x).to_string()`, but spelled in one step.
///
/// Also exported under the shorter name `repr` (re-exported from prelude,
/// so it is callable without import). The two names refer to the same
/// function; pick whichever reads better at the call site.
///
/// Note: this is the **debug** rendering, which differs from `T::to_string`
/// (the `Show` rendering) for some types — most visibly, strings and chars
/// are quoted and escaped here. Use `@debug.to_string` / `repr` for
/// developer-facing output (logs, snapshots, error messages), and the
/// `Show` `to_string` for user-facing output.
///
/// # Examples
/// ```mbt check
/// test {
/// let str = @debug.to_string([1, 2, 3])
/// @test.assert_eq(str, "[1, 2, 3]")
/// // Same function under a shorter name.
/// @test.assert_eq(repr("hi"), "\"hi\"")
/// }
/// ```
#alias(repr)
pub fn[T : Debug] to_string(x : T) -> String {
render(x.to_repr())
}