use super::core::*;
use super::*;
pub(crate) fn render_arithmetic_expr_to_buf(
rendered: &mut String,
expr: &ArithmeticExprNode,
context: RenderContext<'_, '_>,
) {
push_arithmetic_expr(rendered, expr, ArithmeticContext::TopLevel, false, context);
}
pub(super) fn render_arithmetic_subscript_expr_to_buf(
rendered: &mut String,
expr: &ArithmeticExprNode,
context: RenderContext<'_, '_>,
compact: bool,
) {
push_arithmetic_expr(
rendered,
expr,
ArithmeticContext::Subscript,
compact,
context,
);
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum ArithmeticContext {
TopLevel,
Unary,
Postfix,
Binary(ArithmeticBinaryOp),
Assignment,
ConditionalCondition,
ConditionalBranch,
Subscript,
}
pub(super) fn push_arithmetic_expr(
rendered: &mut String,
expr: &ArithmeticExprNode,
context: ArithmeticContext,
compact: bool,
env: RenderContext<'_, '_>,
) {
let needs_parentheses = arithmetic_needs_parentheses(expr, context);
if needs_parentheses {
rendered.push('(');
}
match &expr.kind {
ArithmeticExpr::Number(number) => rendered.push_str(number.slice(env.source)),
ArithmeticExpr::Variable(name) => rendered.push_str(name),
ArithmeticExpr::Indexed { name, index } => {
rendered.push_str(name);
rendered.push('[');
push_arithmetic_expr(rendered, index, ArithmeticContext::Subscript, true, env);
rendered.push(']');
}
ArithmeticExpr::ShellWord(word) => {
let word = render_arithmetic_shell_word(word, env);
if compact {
rendered.push_str(&compact_dynamic_arithmetic_subscript(
word.trim_matches([' ', '\t', '\r']),
));
} else {
rendered.push_str(&word);
}
}
ArithmeticExpr::Parenthesized { expression } => {
rendered.push('(');
push_arithmetic_expr(
rendered,
expression,
ArithmeticContext::TopLevel,
compact,
env,
);
rendered.push(')');
}
ArithmeticExpr::Unary { op, expr } => {
rendered.push_str(arithmetic_unary_operator(*op));
push_arithmetic_expr(rendered, expr, ArithmeticContext::Unary, compact, env);
}
ArithmeticExpr::Postfix { expr, op } => {
push_arithmetic_expr(rendered, expr, ArithmeticContext::Postfix, compact, env);
rendered.push_str(arithmetic_postfix_operator(*op));
}
ArithmeticExpr::Binary { left, op, right } => {
push_arithmetic_expr(rendered, left, ArithmeticContext::Binary(*op), compact, env);
if !compact {
rendered.push(' ');
}
rendered.push_str(arithmetic_binary_operator(*op));
if !compact {
rendered.push(' ');
}
push_arithmetic_expr(
rendered,
right,
ArithmeticContext::Binary(*op),
compact,
env,
);
}
ArithmeticExpr::Conditional {
condition,
then_expr,
else_expr,
} => {
push_arithmetic_expr(
rendered,
condition,
ArithmeticContext::ConditionalCondition,
compact,
env,
);
rendered.push_str(if compact { "?" } else { " ? " });
push_arithmetic_expr(
rendered,
then_expr,
ArithmeticContext::ConditionalBranch,
compact,
env,
);
rendered.push_str(if compact { ":" } else { " : " });
push_arithmetic_expr(
rendered,
else_expr,
ArithmeticContext::ConditionalBranch,
compact,
env,
);
}
ArithmeticExpr::Assignment { target, op, value } => {
push_arithmetic_lvalue(rendered, target, env);
if !compact {
rendered.push(' ');
}
rendered.push_str(arithmetic_assign_operator(*op));
if !compact {
rendered.push(' ');
}
push_arithmetic_expr(rendered, value, ArithmeticContext::Assignment, compact, env);
}
}
if needs_parentheses {
rendered.push(')');
}
}
pub(super) fn push_arithmetic_expansion_body(
rendered: &mut String,
expr: &ArithmeticExprNode,
env: RenderContext<'_, '_>,
) {
let mut body = String::new();
push_arithmetic_expr(&mut body, expr, ArithmeticContext::TopLevel, false, env);
if body.contains("$(")
|| body.contains('`')
|| arithmetic_expr_contains_command_substitution(expr)
{
rendered.push_str(body.trim_matches([' ', '\t', '\r']));
} else {
rendered.push_str(&body);
}
}
pub(super) fn arithmetic_expr_contains_command_substitution(expr: &ArithmeticExprNode) -> bool {
match &expr.kind {
ArithmeticExpr::ShellWord(word) => word.parts.iter().any(|part| {
matches!(
part.kind,
WordPart::CommandSubstitution { .. } | WordPart::ProcessSubstitution { .. }
)
}),
ArithmeticExpr::Indexed { index, .. } => {
arithmetic_expr_contains_command_substitution(index)
}
ArithmeticExpr::Parenthesized { expression } => {
arithmetic_expr_contains_command_substitution(expression)
}
ArithmeticExpr::Unary { expr, .. } | ArithmeticExpr::Postfix { expr, .. } => {
arithmetic_expr_contains_command_substitution(expr)
}
ArithmeticExpr::Binary { left, right, .. } => {
arithmetic_expr_contains_command_substitution(left)
|| arithmetic_expr_contains_command_substitution(right)
}
ArithmeticExpr::Conditional {
condition,
then_expr,
else_expr,
} => {
arithmetic_expr_contains_command_substitution(condition)
|| arithmetic_expr_contains_command_substitution(then_expr)
|| arithmetic_expr_contains_command_substitution(else_expr)
}
ArithmeticExpr::Assignment { target, value, .. } => {
arithmetic_lvalue_contains_command_substitution(target)
|| arithmetic_expr_contains_command_substitution(value)
}
ArithmeticExpr::Number(_) | ArithmeticExpr::Variable(_) => false,
}
}
pub(super) fn arithmetic_lvalue_contains_command_substitution(target: &ArithmeticLvalue) -> bool {
match target {
ArithmeticLvalue::Variable(_) => false,
ArithmeticLvalue::Indexed { index, .. } => {
arithmetic_expr_contains_command_substitution(index)
}
}
}
pub(super) fn render_arithmetic_shell_word(word: &Word, context: RenderContext<'_, '_>) -> String {
let render_with_context = || {
let mut rendered = String::new();
render_word_syntax_internal(word, context, true, &mut rendered);
rendered
};
if context.options.simplify() || context.options.minify() {
let [part] = word.parts.as_slice() else {
return render_with_context();
};
return match &part.kind {
WordPart::Variable(name) => name.to_string(),
WordPart::ArrayAccess(reference) if reference.subscript.is_none() => {
reference.name.to_string()
}
WordPart::Parameter(parameter)
if is_plain_arithmetic_identifier(parameter.raw_body.slice(context.source)) =>
{
parameter.raw_body.slice(context.source).to_string()
}
_ => render_with_context(),
};
}
render_with_context()
}
pub(super) fn is_plain_arithmetic_identifier(text: &str) -> bool {
let mut chars = text.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
pub(super) fn arithmetic_needs_parentheses(
expr: &ArithmeticExprNode,
context: ArithmeticContext,
) -> bool {
let expr_prec = arithmetic_precedence(expr);
match context {
ArithmeticContext::TopLevel
| ArithmeticContext::Subscript
| ArithmeticContext::ConditionalCondition => false,
ArithmeticContext::Unary | ArithmeticContext::Postfix => {
expr_prec < arithmetic_precedence_value(ArithmeticBinaryOp::Power)
}
ArithmeticContext::Assignment => expr_prec <= 2,
ArithmeticContext::ConditionalBranch => expr_prec <= 1,
ArithmeticContext::Binary(parent_op) => {
let parent_prec = arithmetic_precedence_value(parent_op);
expr_prec < parent_prec
|| matches!(
expr.kind,
ArithmeticExpr::Assignment { .. } | ArithmeticExpr::Conditional { .. }
) && expr_prec == parent_prec
}
}
}
pub(super) fn arithmetic_precedence(expr: &ArithmeticExprNode) -> u8 {
match &expr.kind {
ArithmeticExpr::Number(_)
| ArithmeticExpr::Variable(_)
| ArithmeticExpr::Indexed { .. }
| ArithmeticExpr::ShellWord(_)
| ArithmeticExpr::Parenthesized { .. } => 100,
ArithmeticExpr::Postfix { .. } => 90,
ArithmeticExpr::Unary { .. } => 80,
ArithmeticExpr::Binary { op, .. } => arithmetic_precedence_value(*op),
ArithmeticExpr::Conditional { .. } => 2,
ArithmeticExpr::Assignment { .. } => 1,
}
}
pub(super) fn arithmetic_precedence_value(op: ArithmeticBinaryOp) -> u8 {
match op {
ArithmeticBinaryOp::Comma => 0,
ArithmeticBinaryOp::LogicalOr => 10,
ArithmeticBinaryOp::LogicalAnd => 20,
ArithmeticBinaryOp::BitwiseOr => 30,
ArithmeticBinaryOp::BitwiseXor => 40,
ArithmeticBinaryOp::BitwiseAnd => 50,
ArithmeticBinaryOp::Equal | ArithmeticBinaryOp::NotEqual => 60,
ArithmeticBinaryOp::LessThan
| ArithmeticBinaryOp::LessThanOrEqual
| ArithmeticBinaryOp::GreaterThan
| ArithmeticBinaryOp::GreaterThanOrEqual => 70,
ArithmeticBinaryOp::ShiftLeft | ArithmeticBinaryOp::ShiftRight => 75,
ArithmeticBinaryOp::Add | ArithmeticBinaryOp::Subtract => 80,
ArithmeticBinaryOp::Multiply | ArithmeticBinaryOp::Divide | ArithmeticBinaryOp::Modulo => {
85
}
ArithmeticBinaryOp::Power => 95,
}
}
pub(super) fn arithmetic_unary_operator(op: ArithmeticUnaryOp) -> &'static str {
match op {
ArithmeticUnaryOp::PreIncrement => "++",
ArithmeticUnaryOp::PreDecrement => "--",
ArithmeticUnaryOp::Plus => "+",
ArithmeticUnaryOp::Minus => "-",
ArithmeticUnaryOp::LogicalNot => "!",
ArithmeticUnaryOp::BitwiseNot => "~",
}
}
pub(super) fn arithmetic_postfix_operator(op: ArithmeticPostfixOp) -> &'static str {
match op {
ArithmeticPostfixOp::Increment => "++",
ArithmeticPostfixOp::Decrement => "--",
}
}
pub(super) fn arithmetic_binary_operator(op: ArithmeticBinaryOp) -> &'static str {
match op {
ArithmeticBinaryOp::Comma => ",",
ArithmeticBinaryOp::Power => "**",
ArithmeticBinaryOp::Multiply => "*",
ArithmeticBinaryOp::Divide => "/",
ArithmeticBinaryOp::Modulo => "%",
ArithmeticBinaryOp::Add => "+",
ArithmeticBinaryOp::Subtract => "-",
ArithmeticBinaryOp::ShiftLeft => "<<",
ArithmeticBinaryOp::ShiftRight => ">>",
ArithmeticBinaryOp::LessThan => "<",
ArithmeticBinaryOp::LessThanOrEqual => "<=",
ArithmeticBinaryOp::GreaterThan => ">",
ArithmeticBinaryOp::GreaterThanOrEqual => ">=",
ArithmeticBinaryOp::Equal => "==",
ArithmeticBinaryOp::NotEqual => "!=",
ArithmeticBinaryOp::BitwiseAnd => "&",
ArithmeticBinaryOp::BitwiseXor => "^",
ArithmeticBinaryOp::BitwiseOr => "|",
ArithmeticBinaryOp::LogicalAnd => "&&",
ArithmeticBinaryOp::LogicalOr => "||",
}
}
pub(super) fn arithmetic_assign_operator(op: ArithmeticAssignOp) -> &'static str {
match op {
ArithmeticAssignOp::Assign => "=",
ArithmeticAssignOp::AddAssign => "+=",
ArithmeticAssignOp::SubAssign => "-=",
ArithmeticAssignOp::MulAssign => "*=",
ArithmeticAssignOp::DivAssign => "/=",
ArithmeticAssignOp::ModAssign => "%=",
ArithmeticAssignOp::ShiftLeftAssign => "<<=",
ArithmeticAssignOp::ShiftRightAssign => ">>=",
ArithmeticAssignOp::AndAssign => "&=",
ArithmeticAssignOp::XorAssign => "^=",
ArithmeticAssignOp::OrAssign => "|=",
}
}
pub(super) fn push_arithmetic_lvalue(
rendered: &mut String,
target: &ArithmeticLvalue,
env: RenderContext<'_, '_>,
) {
match target {
ArithmeticLvalue::Variable(name) => rendered.push_str(name),
ArithmeticLvalue::Indexed { name, index } => {
rendered.push_str(name);
rendered.push('[');
push_arithmetic_expr(rendered, index, ArithmeticContext::Subscript, true, env);
rendered.push(']');
}
}
}
pub(super) fn push_arithmetic_source_text(
rendered: &mut String,
text: &shuck_ast::SourceText,
ast: Option<&ArithmeticExprNode>,
context: RenderContext<'_, '_>,
) {
if let Some(ast) = ast {
match &ast.kind {
ArithmeticExpr::ShellWord(word)
if !context.options.simplify() && !context.options.minify() =>
{
rendered.push_str(&render_arithmetic_slice_shell_word(word, context));
}
_ => render_arithmetic_subscript_expr_to_buf(rendered, ast, context, true),
}
} else {
rendered.push_str(text.slice(context.source));
}
}
pub(super) fn push_parameter_slice_offset(
rendered: &mut String,
text: &shuck_ast::SourceText,
ast: Option<&ArithmeticExprNode>,
context: RenderContext<'_, '_>,
) {
let mut offset = String::new();
push_arithmetic_source_text(&mut offset, text, ast, context);
if offset.starts_with('-') {
rendered.push(' ');
}
rendered.push_str(&offset);
}
pub(super) fn render_arithmetic_slice_shell_word(
word: &Word,
context: RenderContext<'_, '_>,
) -> String {
let [part] = word.parts.as_slice() else {
let mut rendered = String::new();
render_word_syntax_to_buf(word, context, &mut rendered);
return rendered;
};
match &part.kind {
WordPart::ArithmeticExpansion {
expression,
expression_ast,
syntax,
..
} => {
let mut body = String::new();
if let Some(ast) = expression_ast.as_deref() {
render_arithmetic_expr_to_buf(&mut body, ast, context);
} else {
body.push_str(expression.slice(context.source).trim());
}
let (open, close) = match syntax {
ArithmeticExpansionSyntax::DollarParenParen => ("$((", "))"),
ArithmeticExpansionSyntax::LegacyBracket => ("$[", "]"),
};
format!("{open}{body}{close}")
}
_ => {
let mut rendered = String::new();
render_word_syntax_to_buf(word, context, &mut rendered);
rendered
}
}
}
pub(super) fn push_arithmetic_expansion(
rendered: &mut String,
expression: &shuck_ast::SourceText,
expression_ast: Option<&ArithmeticExprNode>,
syntax: ArithmeticExpansionSyntax,
env: RenderContext<'_, '_>,
) {
let expression_source = expression.slice(env.source);
if matches!(syntax, ArithmeticExpansionSyntax::LegacyBracket) {
push_trimmed_arithmetic_expansion_source(rendered, expression_source, syntax);
} else if let Some(formatted) =
format_multiline_arithmetic_expansion_source(expression_source, syntax, expression_ast, env)
{
rendered.push_str(&formatted);
} else if arithmetic_expression_prefers_raw_source(expression_source)
|| !expression.is_source_backed()
{
push_trimmed_arithmetic_expansion_source(rendered, expression_source, syntax);
} else if let Some(expression_ast) = expression_ast {
match syntax {
ArithmeticExpansionSyntax::DollarParenParen => {
rendered.push_str("$((");
push_arithmetic_expansion_body(rendered, expression_ast, env);
rendered.push_str("))");
}
ArithmeticExpansionSyntax::LegacyBracket => unreachable!("handled above"),
}
} else {
push_trimmed_arithmetic_expansion_source(rendered, expression_source, syntax);
}
}
pub(super) fn push_trimmed_arithmetic_expansion_source(
rendered: &mut String,
expression_source: &str,
syntax: ArithmeticExpansionSyntax,
) {
match syntax {
ArithmeticExpansionSyntax::DollarParenParen => {
rendered.push_str("$((");
rendered.push_str(expression_source.trim());
rendered.push_str("))");
}
ArithmeticExpansionSyntax::LegacyBracket => {
rendered.push_str("$[");
rendered.push_str(expression_source.trim());
rendered.push(']');
}
}
}
pub(super) fn format_multiline_arithmetic_expansion_source(
expression_source: &str,
syntax: ArithmeticExpansionSyntax,
expression_ast: Option<&ArithmeticExprNode>,
context: RenderContext<'_, '_>,
) -> Option<String> {
if !matches!(syntax, ArithmeticExpansionSyntax::DollarParenParen)
|| !expression_source.contains('\n')
|| expression_source.contains('`')
{
return None;
}
let operators = multiline_arithmetic_source_trailing_operators(expression_source)?;
let mut rendered_body = String::new();
if let Some(expression_ast) = expression_ast {
render_arithmetic_expr_to_buf(&mut rendered_body, expression_ast, context);
} else {
rendered_body.push_str(expression_source.trim());
}
let lines = split_rendered_arithmetic_body_at_source_operators(&rendered_body, &operators)?;
let mut continuation_indent = String::new();
context
.options
.push_indent_units(&mut continuation_indent, 1);
let lines = lines
.into_iter()
.map(|line| format!("{continuation_indent}{line}"))
.collect::<Vec<_>>();
Some(format!("$((\\\n{}))", lines.join(" \\\n")))
}
pub(super) fn multiline_arithmetic_source_trailing_operators(
expression_source: &str,
) -> Option<Vec<&str>> {
let lines = expression_source
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>();
if lines.len() < 2 {
return None;
}
let mut operators = Vec::with_capacity(lines.len().saturating_sub(1));
for line in &lines[..lines.len() - 1] {
operators.push(arithmetic_source_line_trailing_operator(line)?);
}
Some(operators)
}
pub(super) fn arithmetic_source_line_trailing_operator(line: &str) -> Option<&'static str> {
let trimmed = line.trim_end_matches([' ', '\t', '\r']);
[
"<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "**", "+", "-", "*", "/", "%", "<", ">",
"&", "^", "|",
]
.into_iter()
.find(|operator| trimmed.ends_with(operator))
}
pub(super) fn split_rendered_arithmetic_body_at_source_operators(
rendered_body: &str,
operators: &[&str],
) -> Option<Vec<String>> {
let mut lines = Vec::with_capacity(operators.len() + 1);
let mut rest = rendered_body.trim();
for operator in operators {
let needle = format!(" {operator} ");
let index = rest.find(&needle)?;
let end = index + 1 + operator.len();
lines.push(rest[..end].trim().to_string());
rest = rest[index + needle.len()..].trim_start();
}
if rest.is_empty() {
return None;
}
lines.push(rest.trim().to_string());
Some(lines)
}
pub(super) fn arithmetic_expression_prefers_raw_source(expression_source: &str) -> bool {
expression_source.contains('`')
}
pub(super) fn push_var_ref(
rendered: &mut String,
reference: &VarRef,
context: RenderContext<'_, '_>,
) {
rendered.push_str(reference.name.as_ref());
if let Some(subscript) = &reference.subscript {
rendered.push('[');
if let Some(selector) = subscript.selector() {
rendered.push(match selector {
SubscriptSelector::At => '@',
SubscriptSelector::Star => '*',
});
} else if let Some(ast) = subscript.arithmetic_ast.as_ref() {
let compact = !arithmetic_subscript_prefers_spaced_expression(
subscript.syntax_text(context.source),
);
render_arithmetic_subscript_expr_to_buf(rendered, ast, context, compact);
} else {
rendered.push_str(&compact_dynamic_arithmetic_subscript(
subscript.syntax_text(context.source),
));
}
rendered.push(']');
}
}
pub(super) fn arithmetic_subscript_prefers_spaced_expression(text: &str) -> bool {
let text = text.trim_start_matches([' ', '\t', '\r']);
text.starts_with("$((") || text.starts_with('(')
}
pub(super) fn compact_dynamic_arithmetic_subscript(text: &str) -> String {
let mut rendered = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
let mut dollar_paren_depth = 0usize;
while let Some(ch) = chars.next() {
if ch == '$' && chars.peek().is_some_and(|next| *next == '(') {
rendered.push(ch);
rendered.push(chars.next().expect("peeked '('"));
dollar_paren_depth = dollar_paren_depth.saturating_add(1);
continue;
}
if dollar_paren_depth > 0 {
if ch == '(' {
dollar_paren_depth = dollar_paren_depth.saturating_add(1);
} else if ch == ')' {
dollar_paren_depth = dollar_paren_depth.saturating_sub(1);
}
rendered.push(ch);
continue;
}
if matches!(ch, ' ' | '\t' | '\r')
&& next_is_additive_operator_before_operand(chars.clone())
&& rendered
.chars()
.last()
.is_some_and(|previous| !matches!(previous, ' ' | '\t' | '\r'))
{
continue;
}
if matches!(ch, ' ' | '\t' | '\r')
&& chars.clone().next().is_some_and(|next| next == '%')
&& rendered
.chars()
.last()
.is_some_and(|previous| !matches!(previous, ' ' | '\t' | '\r'))
{
continue;
}
if matches!(ch, '+' | '-')
&& chars
.clone()
.find(|next| !matches!(next, ' ' | '\t' | '\r'))
.is_some_and(is_arithmetic_subscript_operand_start)
{
rendered.push(ch);
while chars
.peek()
.is_some_and(|next| matches!(next, ' ' | '\t' | '\r'))
{
chars.next();
}
continue;
}
if ch == '%' {
rendered.push(ch);
while chars
.peek()
.is_some_and(|next| matches!(next, ' ' | '\t' | '\r'))
{
chars.next();
}
continue;
}
rendered.push(ch);
}
rendered
}
pub(super) fn next_is_additive_operator_before_operand(
mut chars: std::iter::Peekable<std::str::Chars<'_>>,
) -> bool {
let Some(operator) = chars.next() else {
return false;
};
if !matches!(operator, '+' | '-') {
return false;
}
chars
.find(|next| !matches!(next, ' ' | '\t' | '\r'))
.is_some_and(is_arithmetic_subscript_operand_start)
}
pub(super) fn is_arithmetic_subscript_operand_start(ch: char) -> bool {
ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$' | '(' | '{')
}