// 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.
///|
/// Rendered content with line layout and paren preference.
///
/// This is an internal implementation detail of `pretty_print`.
/// Users should rely on `pretty_print_repr` / `pretty_print_delta`.
priv struct Content {
size : Int
lines : Array[String]
needs_parens : Bool
}
///|
/// Rendered content without paren preference metadata.
///
/// This is an internal implementation detail of `pretty_print`.
priv struct ContentParens {
size : Int
lines : Array[String]
}
///|
///|
impl Add for ContentParens with fn add(self, other) {
{ size: self.size + other.size, lines: self.lines + other.lines }
}
///|
/// Empty content value.
fn empty_content() -> Content {
{ size: 0, lines: [], needs_parens: false }
}
///|
/// Single literal token as a `ContentParens`.
fn verbatim(x : String) -> ContentParens {
{ size: 1, lines: [x] }
}
///|
/// Build a `ContentParens` from explicit size and lines.
fn content_parens(size : Int, lines : Array[String]) -> ContentParens {
{ size, lines }
}
///|
/// Render any `Show` value as a leaf content node.
fn leaf(x : String, needs_parens? : Bool = false) -> Content {
{ size: 1, lines: [x], needs_parens }
}
///|
/// Render any `Show` value as a leaf with a type suffix.
///|
/// Map a transformation over the lines of `ContentParens`.
fn with_lines(
r : ContentParens,
f : (Array[String]) -> Array[String],
) -> ContentParens {
{ size: r.size, lines: f(r.lines) }
}
///|
/// Map a transformation over the lines of `Content`.
fn Content::with_lines_content(
r : Content,
f : (Array[String]) -> Array[String],
) -> Content {
{ size: r.size, lines: f(r.lines), needs_parens: r.needs_parens }
}
///|
/// Surround multi-line content with a prefix/suffix, preserving layout.
fn surround_lines(
start : String,
finish : String,
lines : Array[String],
) -> Array[String] {
match lines {
[] => [start + finish]
[item] => [start + item + finish]
[first, .. middle, last] => [start + first, ..middle, last + finish]
}
}
///|
/// Surround content with a prefix/suffix, preserving layout.
fn surround(
start : String,
finish : String,
r : ContentParens,
) -> ContentParens {
with_lines(r, fn(lines) { surround_lines(start, finish, lines) })
}
///|
/// Convert `Content` to `ContentParens` without adding parentheses.
fn Content::no_wrap(c : Content) -> ContentParens {
{ size: c.size, lines: c.lines }
}
///|
/// Mark a content chunk as needing parentheses when embedded.
fn parens(r : ContentParens) -> Content {
{ size: r.size, lines: r.lines, needs_parens: true }
}
///|
/// Mark a content chunk as not needing parentheses when embedded.
fn no_parens(r : ContentParens) -> Content {
{ size: r.size, lines: r.lines, needs_parens: false }
}
///|
/// Compact multi-line output into a single line when possible.
///
/// Note: we special-case common layouts (brackets/braces/paren-calls)
/// so that a multi-line output like:
///
/// ```text
/// [
/// a,
/// b,
/// ]
/// ```
///
/// compacts into:
///
/// ```text
/// [a, b]
/// ```
fn compact_lines(lines : Array[String]) -> Array[String] {
match lines {
[] => []
[x] => [x]
[first, .. middle, last] =>
// Special-case bracket/brace blocks: "[ ... ]" and "{ ... }"
// produced by `bracket_seq_lines`.
if (first == "[" && last == "]") || (first == "{" && last == "}") {
let parts : Array[StringView] = []
for m in middle {
let t = m.trim()
if t != "" {
parts.push(t)
}
}
let joined0 = parts.join(" ")
let joined : StringView = match joined0.strip_suffix(",") {
Some(sv) => sv
None => joined0
}
// Match expected style:
// - arrays: [a, b]
// - records/maps: { field: value, field: value }
if first == "{" {
let s1 = match joined.strip_prefix(" ") {
Some(sv) => sv
None => joined
}
let inner = match s1.strip_suffix(" ") {
Some(sv) => sv
None => s1
}
if inner == "" {
["{}"]
} else {
["{ \{inner} }"]
}
} else {
["[\{joined}]"]
}
// Special-case parenthesized call-like blocks produced by `comma_seq_lines`:
//
// ```text
// Enum(
// a,
// b,
// )
// ```
//
// compacts into:
//
// ```text
// Enum(a, b)
// ```
} else if first.has_suffix("(") && last == ")" {
let parts : Array[StringView] = []
for m in middle {
let t = m.trim()
if t != "" {
parts.push(t)
}
}
let joined0 = parts.join(" ")
let joined : StringView = match joined0.strip_suffix(",") {
Some(sv) => sv
None => joined0
}
["\{first}\{joined}\{last}"]
} else {
let parts : Array[StringView] = [first]
for m in middle {
parts.push(m.trim())
}
parts.push(last.trim())
[parts.join(" ")]
}
}
}
///|
/// Compact a `Content` value.
fn Content::compact(r : Content) -> Content {
r.with_lines_content(compact_lines)
}
///|
/// Indent every line by a fixed prefix.
fn indent(prefix : String, r : ContentParens) -> ContentParens {
with_lines(r, lines => lines.map(line => prefix + line))
}
///|
/// Indent every line by a fixed number of spaces.
fn indent_spaces(n : Int, r : ContentParens) -> ContentParens {
indent(" ".repeat(n), r)
}
///|
/// Render a bracket/brace sequence.
///
/// Multi-line layout is:
///
/// ```text
/// [
/// a,
/// b,
/// ]
/// ```
///
/// Compact layout (handled by `compact_lines`) is:
///
/// ```text
/// [a, b]
/// ```
fn bracket_seq_lines(
open : String,
close : String,
indent_by : Int,
contents : Array[Array[String]],
) -> Array[String] {
match contents {
[] => [open + close]
[item] =>
// If the item itself is multi-line, keep the surrounding brackets/braces
// multi-line too (important for nested structures when threshold=0).
// Also ensure the single element still gets a trailing comma in multi-line mode.
if item.length() > 1 {
let lines = indent_spaces(indent_by, { size: 0, lines: item }).lines
if !lines.is_empty() {
let last_i = lines.length() - 1
lines[last_i] += ","
}
[open, ..lines, close]
} else {
// single-line item stays on one line
let inner = compact_lines(item)
match inner {
[] => [open + close]
[x] =>
if open == "{" && close == "}" {
let inner = x.trim()
if inner == "" {
["{}"]
} else {
["{ \{inner} }"]
}
} else {
[open + x + close]
}
_ =>
[
open,
..indent_spaces(indent_by, { size: 0, lines: item }).lines,
close,
]
}
}
_ => {
// Multi-line always uses trailing comma style to match expectation.
let out : Array[String] = [open]
for item in contents {
let item_lines = item.filter(line => line != "")
match item_lines {
[] => ()
[x] => out.push(" ".repeat(indent_by) + x + ",")
[first, .. middle, last] => {
out.push(" ".repeat(indent_by) + first)
for mid in middle {
out.push(" ".repeat(indent_by) + mid)
}
out.push(" ".repeat(indent_by) + last + ",")
}
}
}
out.push(close)
out
}
}
}
///|
/// Join content chunks with commas and consistent indentation.
///
/// This is used for function call arguments etc.
fn comma_seq_lines(
begin : String,
end : String,
contents : Array[Array[String]],
) -> Array[String] {
// Special-case common bracket/brace forms to get nicer pretty-print output.
if begin == "[" && end == "]" {
bracket_seq_lines("[", "]", 2, contents)
} else if begin == "{" && end == "}" {
bracket_seq_lines("{", "}", 2, contents)
} else if begin == "" && end == "" {
// Used by some internal layouts (e.g. opaque wrappers).
// Keep the legacy behavior to avoid introducing an empty leading/trailing line.
fn spacer(begin : String) -> String {
" ".repeat(begin.length())
}
let lines = match contents {
[] => [begin + end]
[item] => surround_lines(begin, end, item)
[first, .. middle, last] => {
let space = spacer(begin)
let middle_lines = []
for item in middle {
middle_lines.append(surround_lines(space, ",", item))
}
[
..surround_lines(begin, ",", first),
..middle_lines,
..surround_lines(space, end, last),
]
}
}
lines.filter(line => line != "")
} else {
// Multi-line layout with trailing comma style:
//
// ```text
// Enum(
// a,
// b,
// )
// ```
let indent_by = 2
match contents {
[] => [begin + end]
[item] => {
let item_lines = item.filter(line => line != "")
match item_lines {
[] => [begin + end]
[x] => [begin + x + end]
[.. pre_lines, last_line] =>
[
begin,
..pre_lines.iter().map(line => " ".repeat(indent_by) + line),
" ".repeat(indent_by) + last_line + ",",
end,
]
}
}
_ => {
let out : Array[String] = [begin]
for item in contents {
let item_lines = item.filter(line => line != "")
match item_lines {
[] => ()
[x] => out.push(" ".repeat(indent_by) + x + ",")
[first, .. middle, last] => {
out.push(" ".repeat(indent_by) + first)
for mid in middle {
out.push(" ".repeat(indent_by) + mid)
}
out.push(" ".repeat(indent_by) + last + ",")
}
}
}
out.push(end)
out
}
}
}
}
///|
/// Build a comma-separated sequence as `Content`.
fn comma_seq(
begin : String,
end : String,
contents : Array[ContentParens],
) -> Content {
for c in contents; size = 0 {
continue size + c.size
} nobreak {
{
size,
lines: comma_seq_lines(begin, end, contents.map(c => c.lines)),
needs_parens: false,
}
}
}
///|
/// Convert content into a final `String` with newline separators.
fn print_content(r : ContentParens) -> String {
r.lines.join("\n")
}
///|
/// Render compactly when the compact single-line layout stays under `threshold`.
///
/// `threshold` is treated as a *maximum width hint* (not a structural node count).
/// This fixes cases where a structurally small tree expands to a very long single
/// line (e.g. deeply nested `Cons(...)`).
fn with_resizing(
_root_size : Int,
threshold : Int,
rendered_children : Content,
) -> Content {
// `threshold=0` is used by tests/users to force multi-line output.
// In that mode we should never run the compaction pass.
if threshold <= 0 {
rendered_children
} else {
let compacted = rendered_children.compact()
// Compacting is intended to produce a single line for the current node.
// Still, we use the max line width to be safe.
// `String::length()` counts UTF-16 code units; this is good enough for a
// debug pretty printer.
if compacted.lines.all(line => line.length() <= threshold) {
compacted
} else {
rendered_children
}
}
}