use super::*;
#[derive(Clone, Copy)]
pub(crate) enum SpecialValueCall {
Sorted,
CreateWorkshopSetting,
SettingFactory,
}
pub(crate) const SPECIAL_VALUE_CALLS: &[(&str, SpecialValueCall)] = &[
("sorted", SpecialValueCall::Sorted),
(
"createWorkshopSetting",
SpecialValueCall::CreateWorkshopSetting,
),
(
"createWorkshopSettingBool",
SpecialValueCall::SettingFactory,
),
(
"createWorkshopSettingEnum",
SpecialValueCall::SettingFactory,
),
("createWorkshopSettingInt", SpecialValueCall::SettingFactory),
(
"createWorkshopSettingFloat",
SpecialValueCall::SettingFactory,
),
(
"createWorkshopSettingHero",
SpecialValueCall::SettingFactory,
),
];
impl SpecialValueCall {
pub(crate) fn from_name(name: &str) -> Option<SpecialValueCall> {
SPECIAL_VALUE_CALLS
.iter()
.find(|(spelling, _)| *spelling == name)
.map(|(_, special)| *special)
}
}
fn literal_number(expr: &Expr) -> Option<f64> {
match expr {
Expr::Number { value, .. } => Some(*value),
Expr::Unary { op, operand, .. } if matches!(op.as_str(), "+" | "-") => {
literal_number(operand).map(|value| if op == "-" { -value } else { value })
}
_ => None,
}
}
fn compressed_literal_values(expr: &Expr) -> Option<Vec<f64>> {
match expr {
Expr::Null { .. } => Some(vec![0.0]),
Expr::Number { .. } | Expr::Unary { .. } => literal_number(expr).map(|value| vec![value]),
Expr::Call { name, args, .. } if name == "vect" && args.len() == 3 => Some(
args.iter()
.map(|arg| literal_number(&arg.value))
.collect::<Option<Vec<_>>>()?,
),
_ => None,
}
}
fn is_compressible_column(values: &[&Expr]) -> bool {
let Some(numbers) = values
.iter()
.map(|value| compressed_literal_values(value))
.collect::<Option<Vec<_>>>()
else {
return false;
};
is_compressible_components(&numbers)
}
fn is_compressible_components(values: &[Vec<f64>]) -> bool {
let Some((_, limit)) = crate::lower::compressed_component_mode(values) else {
return false;
};
values.iter().flatten().all(|value| value.abs() < limit)
}
impl Lowerer {
pub(super) fn lower_tabular(
&mut self,
args: &[cst::CallArg],
span: Span,
macro_params: &[String],
) -> Vec<HirStmt> {
let Some(cst::Expr::Array {
elements: targets, ..
}) = args.first().map(|arg| &arg.value)
else {
self.error_at(
"tabular-arguments",
"tabular first argument must be an array of variables".to_string(),
span,
);
return Vec::new();
};
let Some(cst::Expr::Array {
elements: values, ..
}) = args.get(1).map(|arg| &arg.value)
else {
self.error_at(
"tabular-arguments",
"tabular second argument must be an array".to_string(),
span,
);
return Vec::new();
};
if targets.is_empty() {
self.error_at(
"tabular-arguments",
"tabular requires at least one target variable".to_string(),
span,
);
return Vec::new();
}
if values.len() % targets.len() != 0 {
self.error_at(
"tabular-arguments",
format!(
"tabular second argument must have a length that is a multiple of {} (length is {})",
targets.len(),
values.len()
),
args.get(1).map_or(span, |arg| arg.value.span()),
);
return Vec::new();
}
let compress = matches!(
args.get(2).map(|arg| &arg.value),
Some(cst::Expr::Bool { value: true, .. })
);
let mut result = Vec::with_capacity(targets.len());
for (column, target) in targets.iter().enumerate() {
let target = self.lower_expr(target, macro_params, CallPosition::Value);
let column_values_cst = values
.iter()
.skip(column)
.step_by(targets.len())
.collect::<Vec<_>>();
let column_values = column_values_cst
.iter()
.map(|value| self.lower_expr(value, macro_params, CallPosition::Value))
.collect();
let value = HirExpr::Array {
elements: column_values,
span: Some(span.into()),
};
let value = if compress && is_compressible_column(&column_values_cst) {
HirExpr::Call {
name: "compressed".to_string(),
args: vec![value],
debug_source: None,
span: Some(span.into()),
}
} else {
value
};
result.push(HirStmt::Assign {
target: Box::new(target),
value: Box::new(value),
span: Some(span.into()),
});
}
result
}
pub(super) fn lower_split_dict_array(
&mut self,
args: &[cst::CallArg],
span: Span,
macro_params: &[String],
) -> Vec<HirStmt> {
let Some(fields) = args.first().map(|arg| &arg.value) else {
self.error_at(
"split-dict-array-arity",
"splitDictArray requires a field mapping and data array".to_string(),
span,
);
return Vec::new();
};
let Some(rows) = args.get(1).map(|arg| &arg.value) else {
self.error_at(
"split-dict-array-arity",
"splitDictArray requires a field mapping and data array".to_string(),
span,
);
return Vec::new();
};
let Expr::Dict {
entries: field_entries,
..
} = fields
else {
self.error_at(
"split-dict-array-arguments",
"splitDictArray first argument must be a dictionary".to_string(),
fields.span(),
);
return Vec::new();
};
let Expr::Array { elements, .. } = rows else {
self.error_at(
"split-dict-array-arguments",
"splitDictArray second argument must be an array".to_string(),
rows.span(),
);
return Vec::new();
};
let compress = matches!(
args.get(2).map(|arg| &arg.value),
Some(Expr::Bool { value: true, .. })
);
let mut result = Vec::with_capacity(field_entries.len());
for field in field_entries {
let Some(field_name) = expr_identifier(&field.key) else {
self.error_at(
"split-dict-array-key",
"splitDictArray field keys must be identifiers".to_string(),
field.key.span(),
);
continue;
};
let target = self.lower_expr(&field.value, macro_params, CallPosition::Value);
let previous = self.allow_dict_literal;
self.allow_dict_literal = true;
let values: Vec<HirExpr> = elements
.iter()
.map(|element| match element {
Expr::Dict { entries, .. } => entries
.iter()
.find(|entry| expr_identifier(&entry.key) == Some(field_name))
.map(|entry| {
self.lower_expr(&entry.value, macro_params, CallPosition::Value)
})
.unwrap_or(HirExpr::Null { span: None }),
_ => HirExpr::Null { span: None },
})
.collect();
self.allow_dict_literal = previous;
let compressible = compress && is_compressible(&values);
let array = HirExpr::Array {
elements: values,
span: Some(span.into()),
};
let value = if compressible {
HirExpr::Call {
name: "compressed".to_string(),
args: vec![array],
debug_source: None,
span: Some(span.into()),
}
} else {
array
};
result.push(HirStmt::Assign {
target: Box::new(target),
value: Box::new(value),
span: Some(span.into()),
});
}
result
}
pub(super) fn lower_macro_body(&mut self, body: &[Stmt], params: &[String]) -> Vec<HirStmt> {
body.iter()
.map(|stmt| match stmt {
Stmt::Expr { expr, span } => HirStmt::Expr {
expr: Box::new(self.lower_expr(expr, params, CallPosition::MacroBody)),
span: Some(span.into()),
},
_ => self.lower_stmt(stmt, params, false, false),
})
.collect()
}
pub(super) fn lower_special_call(
&mut self,
special: SpecialValueCall,
name: &str,
args: &[cst::CallArg],
span: Span,
macro_params: &[String],
) -> HirExpr {
match special {
SpecialValueCall::CreateWorkshopSetting => {
self.lower_workshop_setting(args, span, macro_params)
}
SpecialValueCall::Sorted => {
for (index, arg) in args.iter().enumerate() {
if let Some((keyword, keyword_span)) = &arg.keyword {
if index != 1 || keyword != "key" {
self.error_at(
"unknown-keyword",
format!(
"unknown keyword argument '{keyword}' for function 'sorted'"
),
*keyword_span,
);
}
}
}
HirExpr::Call {
name: name.to_string(),
args: self.lower_arg_values_with_lambda(args, macro_params, |index, arg| {
index == 1 || arg.keyword.as_ref().is_some_and(|(name, _)| name == "key")
}),
debug_source: None,
span: Some(span.into()),
}
}
SpecialValueCall::SettingFactory => HirExpr::Call {
name: name.to_string(),
args: self.lower_arg_values(args, macro_params),
debug_source: None,
span: Some(span.into()),
},
}
}
pub(super) fn lower_workshop_setting(
&mut self,
args: &[cst::CallArg],
span: Span,
macro_params: &[String],
) -> HirExpr {
if !(4..=5).contains(&args.len()) {
self.error_at(
"invalid-arity",
format!(
"function 'createWorkshopSetting' takes 4 or 5 arguments, received {}",
args.len()
),
span,
);
return HirExpr::Null { span: None };
}
for arg in args {
if let Some((keyword, keyword_span)) = &arg.keyword {
self.error_at(
"keyword-unsupported",
format!(
"function 'createWorkshopSetting' does not accept keyword arguments ('{keyword}')"
),
*keyword_span,
);
}
}
let setting_type = match &args[0].value {
Expr::Type {
name,
args: type_args,
span: type_span,
} => HirExpr::Type {
name: name.clone(),
args: type_args
.iter()
.map(|arg| self.lower_expr(arg, macro_params, CallPosition::Value))
.collect(),
span: Some((*type_span).into()),
},
Expr::Name {
name,
span: type_span,
} if matches!(name.as_str(), "bool" | "int" | "float") => HirExpr::Type {
name: name.clone(),
args: Vec::new(),
span: Some((*type_span).into()),
},
other => {
self.error_at(
"invalid-argument",
"argument 1 of 'createWorkshopSetting' must be a setting type".to_string(),
other.span(),
);
HirExpr::Null { span: None }
}
};
let mut lowered = Vec::with_capacity(5);
lowered.push(setting_type);
lowered.extend(
args[1..]
.iter()
.map(|arg| self.lower_expr(&arg.value, macro_params, CallPosition::Value)),
);
if args.len() == 4 {
lowered.push(HirExpr::Number {
value: 0.0,
text: "0".to_string(),
span: None,
});
}
HirExpr::Call {
name: "createWorkshopSetting".to_string(),
args: lowered,
debug_source: None,
span: Some(span.into()),
}
}
pub(super) fn qualified_macro_for_member(&self, member: &str) -> Option<String> {
let suffix = format!(".{member}");
self.macro_declarations
.iter()
.filter(|(name, order)| name.ends_with(&suffix) && **order <= self.current_order)
.map(|(name, _)| name.clone())
.next()
}
}
fn is_compressible(values: &[HirExpr]) -> bool {
let values = values
.iter()
.map(|value| match value {
HirExpr::Vector { x, y, z, .. } => Some(vec![
crate::hir::visit::literal_number(x)?,
crate::hir::visit::literal_number(y)?,
crate::hir::visit::literal_number(z)?,
]),
_ => crate::hir::visit::literal_number(value).map(|number| vec![number]),
})
.collect::<Option<Vec<_>>>();
values.as_deref().is_some_and(is_compressible_components)
}