///|
/// Whether an element must be quoted so that the joined line can be split
/// back into its elements (#11): it is empty, or contains whitespace, a double
/// quote, or the (non-empty) separator.
fn needs_quotes(part : String, join : String) -> Bool {
if part.is_empty() {
return true
}
if join != "" && part.contains(join) {
return true
}
for c in part {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '"' {
return true
}
}
false
}
///|
/// Renders each element with `Show`, wrapping it in double quotes when
/// `needs_quotes` says so; inside quotes, `\` and `"` are escaped with a
/// backslash. Line breaks stay literal, so multi-line elements still render on
/// several lines (#9).
fn[T : Show] render_parts(a : Array[T], join : String) -> Array[String] {
a.map(x => {
let part = x.to_string()
if !needs_quotes(part, join) {
return part
}
let sb = StringBuilder()
sb.write_char('"')
for c in part {
if c == '\\' || c == '"' {
sb.write_char('\\')
}
sb.write_char(c)
}
sb.write_char('"')
sb.to_string()
})
}
///|
/// Locates the start and end character positions in a flattened string
/// representation of an array sequence.
///
/// Parameters:
///
/// * `seq` : The array of elements to locate positions within.
/// * `pos` : The starting index position in the array.
/// * `offset` : The number of additional elements to include after the starting
/// position.
/// * `join` : The number of characters used to join elements when flattening to
/// string (default: 1).
///
/// Returns a tuple `(start, end)` of UTF-16 offsets into the flattened string:
/// the target range is `start` (inclusive) to `end` (exclusive). Returns
/// `(-1, -1)` if `pos` is negative, `pos` is greater than or equal to the array
/// length, or `offset` is negative; it never panics. Callers clamp `offset`
/// first (see `locate_span`).
///
fn[T : Show] err_locate_arr(
seq : Array[T],
pos : Int32,
offset : Int32,
join~ : Int32 = 1,
) -> (Int32, Int32) {
guard pos >= 0 && pos < seq.length() && offset >= 0 else { (-1, -1) }
let start = seq.foldi(init=0, (i, acc, x) => {
if i < pos {
acc + x.to_string().length()
} else {
acc
}
}) +
join * pos
let end = start +
seq.foldi(init=0, (i, acc, x) => {
if i >= pos && i <= pos + offset {
acc + x.to_string().length()
} else {
acc
}
}) +
join * offset
(start, end)
}
///|
/// Padding that puts the marker under code unit `to` of `text` when the marker
/// line starts at code unit `from`: one space per code unit in between, except
/// that a tab is copied as a tab. The marker then lines up with the text above
/// it whatever tab width the terminal uses (#8).
fn pad_like(text : String, from : Int32, to : Int32) -> String {
let sb = StringBuilder()
for i in from.. (Int32, Int32) {
let start = if start > 0 &&
start < text.length() &&
is_low_surrogate(text[start]) &&
is_high_surrogate(text[start - 1]) {
start - 1
} else {
start
}
let end = if end > 0 &&
end < text.length() &&
is_high_surrogate(text[end - 1]) &&
is_low_surrogate(text[end]) {
end + 1
} else {
end
}
(start, if end < start { start } else { end })
}
///|
fn is_high_surrogate(u : UInt16) -> Bool {
u >= (0xD800 : UInt16) && u <= (0xDBFF : UInt16)
}
///|
fn is_low_surrogate(u : UInt16) -> Bool {
u >= (0xDC00 : UInt16) && u <= (0xDFFF : UInt16)
}
///|
/// Locates the marked span in the rendered `text` as UTF-16 offsets
/// `(start, end)`, `end` exclusive; `start == end` is a zero-width point.
/// Returns `None` when there is no position (`pos < 0`).
fn[T : Show] locate_span(
seq : Sequence[T],
text : String,
pos : Int32,
offset : Int32,
) -> (Int32, Int32)? {
// One rule for both sequence kinds (#10): a negative `pos` means "no
// position"; a negative `offset` counts as 0; a span that runs past the end
// is clamped to the last element; a `pos` at or past the end is a zero-width
// point after the text, drawn as one marker.
if pos < 0 {
return None
}
let offset = if offset < 0 { 0 } else { offset }
let (start, end) = match seq {
Arr(a, j) => {
let n = a.length()
if pos >= n {
(text.length(), text.length())
} else {
// `offset >= n - pos` avoids overflowing `pos + offset`.
let offset = if offset >= n - pos { n - 1 - pos } else { offset }
err_locate_arr(render_parts(a, j), pos, offset, join=j.length())
}
}
Str(s) => {
let n = s.length()
if pos >= n {
(n, n)
} else {
let last = if offset >= n - pos { n - 1 } else { pos + offset }
(pos, last + 1)
}
}
}
Some((start, end))
}
///|
/// Formats an error type and error message into a standardized error string.
///
/// Parameters:
///
/// * `err_type` : An optional error type identifier that will be included in
/// parentheses if present.
/// * `err_msg` : The main error message to be displayed.
///
/// Returns a formatted error string in the format "Error(type): message" if
/// error type is provided, or "Error: message" if no error type is specified.
fn gen_err_msg(err_type : String?, err_msg : String) -> String {
let et = match err_type {
Some(s) => "(\{s})"
None => ""
}
"Error\{et}: \{err_msg}"
}
///|
/// Generates a formatted error visualization string with sequence display,
/// position markers, and an error message.
///
/// Parameters:
///
/// * `self` : The error data containing position, offset, error type, and error
/// message information.
/// * `seq` : The sequence to visualize, either an array with join string or a
/// plain string.
/// * `marker` : The character used to create the visual position indicator
/// (default: '^').
/// * `gen_msg` : A function to format the error type and message into the final
/// error string (default: `gen_err_msg`).
///
/// Returns a multi-line string containing the sequence, position markers, and
/// formatted error message.
pub fn[T : Show] ErrorData::mark(
self : ErrorData,
seq : Sequence[T],
marker~ : Char = '^',
gen_msg~ : (String?, String) -> String = gen_err_msg,
) -> String {
let (pos, offset) = (self.err_pos, self.err_offset)
let seq_str = match seq {
Arr(a, j) => render_parts(a, j).join(j)
Str(s) => s
}
let msg = gen_msg(self.err_type, self.err_msg)
guard locate_span(seq, seq_str, pos, offset) is Some((start, end)) else {
return seq_str + "\n\n" + msg
}
let (start, end) = snap_to_code_points(seq_str, start, end)
// The marker goes right under the line that holds the start of the span,
// and its column is counted from the start of that line. A span that crosses
// a line break is marked up to the end of its first line (#9).
let newline = '\n'.to_int().to_uint16()
let mut line_start = start
while line_start > 0 && seq_str[line_start - 1] != newline {
line_start -= 1
}
let mut line_end = start
while line_end < seq_str.length() && seq_str[line_end] != newline {
line_end += 1
}
let end = if end > line_end { line_end } else { end }
let width = if end > start { end - start } else { 1 }
let marker_str = pad_like(seq_str, line_start, start) +
String::make(width, marker)
let head = seq_str.unsafe_substring(start=0, end=line_end)
if line_end >= seq_str.length() {
head + "\n" + marker_str + "\n" + msg
} else {
let tail = seq_str.unsafe_substring(
start=line_end + 1,
end=seq_str.length(),
)
head + "\n" + marker_str + "\n" + tail + "\n" + msg
}
}