use crate::LuaString;
use crate::ast::{AstBinaryOpKind, AstGlobalAttr, AstGlobalBinding, AstLabelId};
use crate::generate::common::{NumberFormat, QuoteStyle};
use crate::generate::doc::Doc;
use super::{
Assoc, ExprSide, PREC_ADD, PREC_BIT_AND, PREC_BIT_OR, PREC_BIT_XOR, PREC_COMPARE, PREC_CONCAT,
PREC_MUL, PREC_POW, PREC_SHIFT,
};
pub(super) fn maybe_parenthesize(
doc: Doc,
expr_prec: u8,
parent_prec: u8,
side: ExprSide,
assoc: Assoc,
) -> Doc {
let needs_parens = if expr_prec < parent_prec {
true
} else if expr_prec > parent_prec {
false
} else {
match assoc {
Assoc::Left => matches!(side, ExprSide::Right),
Assoc::Right => matches!(side, ExprSide::Left),
Assoc::Non => !matches!(side, ExprSide::Standalone),
Assoc::Full => false,
}
};
if needs_parens {
Doc::concat([Doc::text("("), doc, Doc::text(")")])
} else {
doc
}
}
pub(super) fn binary_meta(op: AstBinaryOpKind) -> (u8, Assoc, &'static str) {
match op {
AstBinaryOpKind::Add => (PREC_ADD, Assoc::Left, "+"),
AstBinaryOpKind::Sub => (PREC_ADD, Assoc::Left, "-"),
AstBinaryOpKind::Mul => (PREC_MUL, Assoc::Left, "*"),
AstBinaryOpKind::Div => (PREC_MUL, Assoc::Left, "/"),
AstBinaryOpKind::FloorDiv => (PREC_MUL, Assoc::Left, "//"),
AstBinaryOpKind::Mod => (PREC_MUL, Assoc::Left, "%"),
AstBinaryOpKind::Pow => (PREC_POW, Assoc::Right, "^"),
AstBinaryOpKind::BitAnd => (PREC_BIT_AND, Assoc::Left, "&"),
AstBinaryOpKind::BitOr => (PREC_BIT_OR, Assoc::Left, "|"),
AstBinaryOpKind::BitXor => (PREC_BIT_XOR, Assoc::Left, "~"),
AstBinaryOpKind::Shl => (PREC_SHIFT, Assoc::Left, "<<"),
AstBinaryOpKind::Shr => (PREC_SHIFT, Assoc::Left, ">>"),
AstBinaryOpKind::Concat => (PREC_CONCAT, Assoc::Right, ".."),
AstBinaryOpKind::Eq => (PREC_COMPARE, Assoc::Non, "=="),
AstBinaryOpKind::Lt => (PREC_COMPARE, Assoc::Non, "<"),
AstBinaryOpKind::Le => (PREC_COMPARE, Assoc::Non, "<="),
}
}
pub(super) fn common_global_attr(bindings: &[AstGlobalBinding]) -> Option<AstGlobalAttr> {
let first = bindings
.first()
.map(|binding| binding.attr)
.unwrap_or(AstGlobalAttr::None);
bindings
.iter()
.all(|binding| binding.attr == first)
.then_some(first)
}
pub(super) fn format_label_name(label: AstLabelId) -> String {
format!("L{}", label.index())
}
pub(super) fn format_number(value: f64, preserve_integral_float: bool) -> String {
if value.is_nan() {
return "(0/0)".to_owned();
}
if value.is_infinite() {
return if value.is_sign_negative() {
"(-1/0)".to_owned()
} else {
"(1/0)".to_owned()
};
}
if value == 0.0 && value.is_sign_negative() {
return "-0.0".to_owned();
}
let rendered = value.to_string();
if !preserve_integral_float || rendered.contains(['.', 'e', 'E']) {
rendered
} else {
format!("{rendered}.0")
}
}
pub(super) fn format_integer(value: i64, number_format: NumberFormat) -> String {
match number_format {
NumberFormat::Decimal if value == i64::MIN => format_signed_hex(value),
NumberFormat::Decimal => value.to_string(),
NumberFormat::Hex => format_signed_hex(value),
}
}
pub(super) fn format_unsigned_integer(value: u64, number_format: NumberFormat) -> String {
match number_format {
NumberFormat::Decimal => value.to_string(),
NumberFormat::Hex => format!("0x{value:x}"),
}
}
fn format_signed_hex(value: i64) -> String {
if value.is_negative() {
format!("-0x{:x}", value.unsigned_abs())
} else {
format!("0x{:x}", value)
}
}
pub(super) fn format_complex_literal(real: f64, imag: f64) -> String {
if real == 0.0 {
return format!("{}i", format_number(imag, false));
}
let imag_abs = format_number(imag.abs(), false);
let imag_sign = if imag.is_sign_negative() { "-" } else { "+" };
format!(
"({} {} {}i)",
format_number(real, false),
imag_sign,
imag_abs
)
}
pub(super) fn format_string_literal(value: &LuaString, quote_style: QuoteStyle) -> String {
if let Some(text) = value.preferred_text().or_else(|| value.as_utf8())
&& can_use_long_bracket_string(text)
{
return format_long_bracket_string(text);
}
let candidates = match quote_style {
QuoteStyle::PreferDouble => ['"', '\''],
QuoteStyle::PreferSingle => ['\'', '"'],
QuoteStyle::MinEscape => ['"', '\''],
};
let preferred = if matches!(quote_style, QuoteStyle::MinEscape) {
if escape_cost(value, '"') <= escape_cost(value, '\'') {
'"'
} else {
'\''
}
} else {
candidates[0]
};
let mut rendered = String::new();
rendered.push(preferred);
push_quoted_string_body(&mut rendered, value, preferred);
rendered.push(preferred);
rendered
}
fn push_quoted_string_body(rendered: &mut String, value: &LuaString, quote: char) {
if let Some(text) = value.preferred_text().or_else(|| value.as_utf8()) {
push_utf8_string_body(rendered, text, quote);
} else {
push_byte_string_body(rendered, value.as_bytes(), quote);
}
}
fn push_utf8_string_body(rendered: &mut String, text: &str, quote: char) {
for ch in text.chars() {
match ch {
'\n' => rendered.push_str("\\n"),
'\r' => rendered.push_str("\\r"),
'\t' => rendered.push_str("\\t"),
'\\' => rendered.push_str("\\\\"),
c if c == quote => {
rendered.push('\\');
rendered.push(c);
}
c if c.is_control() => {
push_escaped_utf8_bytes(rendered, c);
}
c => rendered.push(c),
}
}
}
fn push_byte_string_body(rendered: &mut String, bytes: &[u8], quote: char) {
for byte in bytes {
match *byte {
b'\n' => rendered.push_str("\\n"),
b'\r' => rendered.push_str("\\r"),
b'\t' => rendered.push_str("\\t"),
b'\\' => rendered.push_str("\\\\"),
b if char::from(b) == quote => {
rendered.push('\\');
rendered.push(char::from(b));
}
0x20..=0x7e => rendered.push(char::from(*byte)),
byte => push_decimal_escape(rendered, byte),
}
}
}
fn push_escaped_utf8_bytes(rendered: &mut String, ch: char) {
let mut bytes = [0; 4];
for byte in ch.encode_utf8(&mut bytes).as_bytes() {
push_decimal_escape(rendered, *byte);
}
}
fn push_decimal_escape(rendered: &mut String, byte: u8) {
rendered.push('\\');
rendered.push(char::from(b'0' + byte / 100));
rendered.push(char::from(b'0' + byte / 10 % 10));
rendered.push(char::from(b'0' + byte % 10));
}
fn can_use_long_bracket_string(value: &str) -> bool {
value.contains('\n') && !value.starts_with('\n') && !value.contains('\r')
}
fn format_long_bracket_string(value: &str) -> String {
let eqs = long_bracket_eqs(value);
format!("[{eqs}[{value}]{eqs}]")
}
fn long_bracket_eqs(value: &str) -> String {
for count in 0.. {
let eqs = "=".repeat(count);
let closing = format!("]{eqs}]");
if !value.contains(&closing) {
return eqs;
}
}
unreachable!("unbounded search over bracket delimiters should always terminate")
}
fn escape_cost(value: &LuaString, quote: char) -> usize {
if let Some(text) = value.preferred_text().or_else(|| value.as_utf8()) {
return text
.chars()
.map(|ch| match ch {
'\n' | '\r' | '\t' | '\\' => 2,
c if c == quote => 2,
c if c.is_control() => c.len_utf8() * 4,
_ => ch.len_utf8(),
})
.sum();
}
value
.as_bytes()
.iter()
.map(|byte| match *byte {
b'\n' | b'\r' | b'\t' | b'\\' => 2,
b if char::from(b) == quote => 2,
0x20..=0x7e => 1,
_ => 4,
})
.sum()
}