use crate::output::emitter::*;
impl<'a> EmitContext<'a> {
pub(crate) fn value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
let Some(node) = self.program.values.get(id) else {
return Err(WorkshopError::malformed(
format!("dangling value {id}"),
None,
));
};
match &node.value {
wir::Value::Number { text, .. } => {
out.push_str(text);
}
wir::Value::String(value) => {
self.emit_string_value(value, out)?;
}
wir::Value::LocalizedString(id) => {
out.push_str(self.spelling(Kind::Value, "string")?);
out.push('(');
let spelling = self.localized_string_spelling(id)?;
write!(out, "\"{}\"", escape_value_string(spelling)).unwrap();
out.push(')');
}
wir::Value::Bool(true) => out.push_str(self.spelling(Kind::Value, "true")?),
wir::Value::Bool(false) => out.push_str(self.spelling(Kind::Value, "false")?),
wir::Value::Null => out.push_str(self.spelling(Kind::Value, "null")?),
wir::Value::Array(elements) => {
if elements.is_empty() {
out.push_str(self.spelling(Kind::Value, "emptyArray")?);
} else {
out.push_str(self.spelling(Kind::Value, "array")?);
out.push('(');
self.args(elements, out)?;
out.push(')');
}
}
wir::Value::Vector { x, y, z } => {
out.push_str(self.spelling(Kind::Value, "vector")?);
out.push('(');
self.value(*x, out)?;
out.push_str(", ");
self.value(*y, out)?;
out.push_str(", ");
self.value(*z, out)?;
out.push(')');
}
wir::Value::Enum { value_type, value } => {
let spelling = self.enum_spelling(value_type, value)?;
out.push_str(
&self
.catalog
.enum_member_form(value_type, spelling, &self.locale),
);
}
wir::Value::GlobalVariable(variable) => {
let name = self.global_name(*variable)?;
let global = self.spelling_prefer(Kind::Structural, "global", "Global")?;
write!(out, "{global}.{name}").unwrap();
}
wir::Value::PlayerVariable { player, variable } => {
out.push('(');
self.value(*player, out)?;
out.push(')');
let name = self.player_name(*variable)?;
write!(out, ".{name}").unwrap();
}
wir::Value::Subroutine(subroutine) => {
let name = self
.program
.subroutines
.get(*subroutine)
.map(|value| value.name.clone())
.ok_or_else(|| {
WorkshopError::malformed(
format!("dangling subroutine value {subroutine}"),
None,
)
})?;
out.push_str(&name);
}
wir::Value::EventPlayer => out.push_str(self.spelling(Kind::Value, "eventPlayer")?),
wir::Value::Call { name, args } if name == wir::AMBIGUOUS_ENUM_CALL => {
out.push_str(&self.ambiguous_enum_spelling_from_args(args)?);
}
wir::Value::Call { name, args } => {
if name == "memberAccess" {
if args.len() < 2 || args.len() > 3 {
return Err(WorkshopError::malformed(
"memberAccess expects two or three arguments".to_string(),
node.span,
));
}
let Some(wir::ValueNode {
value: wir::Value::String(member),
..
}) = self.program.values.get(args[1])
else {
return Err(WorkshopError::malformed(
"memberAccess member must be a string".to_string(),
node.span,
));
};
let bare_event_player = self
.program
.values
.get(args[0])
.is_some_and(|node| matches!(node.value, wir::Value::EventPlayer));
if !bare_event_player {
out.push('(');
}
self.value(args[0], out)?;
if !bare_event_player {
out.push(')');
}
write!(out, ".{member}").unwrap();
if let Some(index) = args.get(2) {
out.push('[');
self.value(*index, out)?;
out.push(']');
}
return Ok(());
}
if wir::is_comparison_operator(name) {
if args.len() != 2 {
return Err(WorkshopError::malformed(
format!("comparison call '{name}' must have 2 args"),
None,
));
}
out.push_str(self.spelling(Kind::Value, "compare")?);
out.push('(');
self.value(args[0], out)?;
write!(out, ", {name}, ").unwrap();
self.value(args[1], out)?;
out.push(')');
return Ok(());
}
if name == "-" && args.len() == 1 {
out.push_str(self.spelling(Kind::Value, "multiply")?);
out.push_str("(-1, ");
self.value(args[0], out)?;
out.push(')');
return Ok(());
}
if name == "getAllPlayers" && args.is_empty() {
out.push_str(self.spelling(Kind::Value, "allPlayers")?);
out.push('(');
out.push_str(self.enum_spelling("Team", "ALL")?);
out.push(')');
return Ok(());
}
let canonical = match name.as_str() {
"+" => Some("add"),
"-" => Some("subtract"),
"*" => Some("multiply"),
"/" => Some("divide"),
"len" => Some("countOf"),
"abs" => Some("absoluteValue"),
"sqrt" => Some("squareRoot"),
"createBeam" => Some("createBeamEffect"),
"random.uniform" => Some("randomReal"),
"random.choice" => Some("randomValueInArray"),
"format" => Some("customString"),
_ => None,
};
let spelling = if let Some(canonical) = canonical {
self.spelling(Kind::Value, canonical)?
} else {
self.spelling(Kind::Value, name)?
};
let is_custom_string = matches!(name.as_str(), "format" | "customString");
if name == "string" {
out.push_str(spelling);
out.push('(');
if let Some(first) = args.first() {
self.localized_string_value(*first, out)?;
if args.len() > 1 {
out.push_str(", ");
self.args(&args[1..], out)?;
}
}
out.push(')');
} else if args.is_empty() {
out.push_str(spelling);
} else if is_custom_string {
match self.canonicalize_format_call(args)? {
Some((text, variable_args)) => {
if variable_args.is_empty() {
self.emit_string_value(&text, out)?;
} else {
out.push_str(spelling);
out.push('(');
write!(out, "\"{}\"", escape_value_string(&text)).unwrap();
if !variable_args.is_empty() {
out.push_str(", ");
}
self.args(&variable_args, out)?;
out.push(')');
}
}
None => {
out.push_str(spelling);
out.push('(');
self.bare_string_value(args[0], out)?;
if args.len() > 1 {
out.push_str(", ");
}
self.args(&args[1..], out)?;
out.push(')');
}
}
} else {
out.push_str(spelling);
out.push('(');
self.args(args, out)?;
out.push(')');
}
}
}
Ok(())
}
pub(crate) fn localized_string_value(
&mut self,
id: wir::ValueId,
out: &mut String,
) -> Result<()> {
let Some(node) = self.program.values.get(id) else {
return Err(WorkshopError::malformed(
format!("dangling value {id}"),
None,
));
};
let wir::Value::LocalizedString(id) = &node.value else {
return Err(WorkshopError::unsupported(
"value 'string' argument 1 must be localized string text",
node.span,
));
};
let spelling = self.localized_string_spelling(id)?;
write!(out, "\"{}\"", escape_value_string(spelling)).unwrap();
Ok(())
}
pub(crate) fn canonicalize_format_call(
&self,
args: &[wir::ValueId],
) -> Result<Option<(String, Vec<wir::ValueId>)>> {
if args.len() < 2 {
return Ok(None);
}
let Some(text) = self.program.values.get(args[0]) else {
return Ok(None);
};
let wir::Value::String(text) = &text.value else {
return Ok(None);
};
let format_args = &args[1..];
let mut has_implicit = false;
let mut has_explicit = false;
let mut out_of_range = false;
let mut cursor = 0usize;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut inner = String::new();
let mut closed = false;
for next in chars.by_ref() {
if next == '}' {
closed = true;
break;
}
inner.push(next);
}
if !closed {
break;
}
if inner.is_empty() {
if cursor >= format_args.len() {
out_of_range = true;
}
cursor += 1;
has_implicit = true;
} else if inner.chars().all(|c| c.is_ascii_digit()) {
match inner.parse::<usize>() {
Ok(index) if index < format_args.len() => has_explicit = true,
_ => out_of_range = true,
}
} else {
out_of_range = true;
}
}
}
if out_of_range || (has_implicit && has_explicit) {
return Ok(None);
}
let mut any_constant = false;
for id in format_args {
let Some(node) = self.program.values.get(*id) else {
return Ok(None);
};
if matches!(node.value, wir::Value::Number { .. }) {
any_constant = true;
}
}
if !has_implicit && !any_constant {
return Ok(None);
}
let mut canonical = String::with_capacity(text.len());
let mut variable_args = Vec::new();
let mut variable_index = 0usize;
let mut cursor = 0usize;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut inner = String::new();
let mut closed = false;
for next in chars.by_ref() {
if next == '}' {
closed = true;
break;
}
inner.push(next);
}
if !closed {
canonical.push('{');
canonical.push_str(&inner);
break;
}
let index = if inner.is_empty() {
let index = cursor;
cursor += 1;
index
} else {
match inner.parse::<usize>() {
Ok(index) => index,
Err(_) => {
canonical.push('{');
canonical.push_str(&inner);
canonical.push('}');
continue;
}
}
};
let Some(arg) = format_args.get(index).copied() else {
canonical.push('{');
canonical.push_str(&inner);
canonical.push('}');
continue;
};
let node = self.program.values.get(arg);
if let Some(wir::Value::Number { value, .. }) = node.map(|node| &node.value) {
canonical.push_str(&fold_number(*value));
} else {
write!(canonical, "{{{variable_index}}}").unwrap();
variable_index += 1;
variable_args.push(arg);
}
} else {
canonical.push(ch);
}
}
Ok(Some((canonical, variable_args)))
}
pub(crate) fn modify_op_spelling(&mut self, op: wir::ModifyOp) -> Result<&'a str> {
self.spelling(Kind::Operator, op.catalog_id())
}
pub(crate) fn spelling(&mut self, kind: Kind, id: &str) -> Result<&'a str> {
let Some(entry) = self.catalog.entry(kind, id) else {
return Err(WorkshopError::unknown(
kind.as_str(),
id.to_string(),
self.locale.clone(),
None,
)
.with_candidates(crate::core::suggest::nearest(
id,
self.catalog
.canonical_spellings(kind, self.catalog.primary_locale()),
crate::core::suggest::CANDIDATE_LIMIT,
)));
};
self.localized(kind.as_str(), id, id.to_string(), |locale| {
entry.spelling(locale)
})
}
fn localized(
&mut self,
kind: &'static str,
id: &str,
fallback_id: String,
resolve: impl Fn(&Locale) -> Option<&'a str>,
) -> Result<&'a str> {
self.localized_with(kind, id, fallback_id, false, resolve)
}
pub(crate) fn localized_with<T>(
&mut self,
kind: &'static str,
id: &str,
fallback_id: String,
dedup: bool,
resolve: impl Fn(&Locale) -> Option<T>,
) -> Result<T> {
if let Some(spelling) = resolve(&self.locale) {
return Ok(spelling);
}
if let Some(fallback) = &self.fallback
&& let Some(spelling) = resolve(fallback)
{
if !dedup || !self.fallback_ids.contains(&fallback_id) {
self.fallback_ids.push(fallback_id);
}
return Ok(spelling);
}
Err(WorkshopError::MissingMapping {
kind,
id: id.to_string(),
locale: self.locale.clone(),
})
}
pub(crate) fn localized_string_spelling(&mut self, id: &str) -> Result<&'a str> {
if !self.catalog.localized_strings().any(|entry| entry.id == id) {
return Err(WorkshopError::unknown(
"localized string",
id.to_string(),
self.locale.clone(),
None,
)
.with_candidates(crate::core::suggest::nearest(
id,
self.catalog
.localized_strings()
.map(|entry| entry.id.as_str()),
crate::core::suggest::CANDIDATE_LIMIT,
)));
}
self.localized(
"localized string",
id,
format!("localizedString.{id}"),
|locale| self.catalog.localized_string_spelling(locale, id),
)
}
pub(crate) fn spelling_prefer(
&mut self,
kind: Kind,
id: &str,
preferred: &str,
) -> Result<&'a str> {
let Some(entry) = self.catalog.entry(kind, id) else {
return Err(WorkshopError::unknown(
kind.as_str(),
id.to_string(),
self.locale.clone(),
None,
)
.with_candidates(crate::core::suggest::nearest(
id,
self.catalog
.canonical_spellings(kind, self.catalog.primary_locale()),
crate::core::suggest::CANDIDATE_LIMIT,
)));
};
self.localized(kind.as_str(), id, id.to_string(), |locale| {
entry
.spellings(locale)
.iter()
.find(|spelling| spelling.as_str() == preferred)
.map(String::as_str)
.or_else(|| entry.spelling(locale))
})
}
pub(crate) fn structural(&mut self, id: &str) -> Result<&'a str> {
self.spelling(Kind::Structural, id)
}
pub(crate) fn enum_spelling(&mut self, domain: &str, member: &str) -> Result<&'a str> {
let Some(domain_entry) = self.catalog.enum_domain(domain) else {
return Err(WorkshopError::unknown(
"enum domain",
domain.to_string(),
self.locale.clone(),
None,
)
.with_candidates(crate::core::suggest::nearest(
domain,
self.catalog
.enum_domains()
.map(|domain| domain.domain.as_str()),
crate::core::suggest::CANDIDATE_LIMIT,
)));
};
let Some(member_entry) = domain_entry.members.iter().find(|m| m.member == member) else {
return Err(WorkshopError::unknown(
"enum member",
format!("{domain}.{member}"),
self.locale.clone(),
None,
)
.with_candidates(crate::core::suggest::nearest(
member,
self.catalog
.canonical_member_spellings(domain, self.catalog.primary_locale()),
crate::core::suggest::CANDIDATE_LIMIT,
)));
};
self.localized(
"enum member",
&format!("{domain}.{member}"),
format!("{domain}.{member}"),
|locale| member_entry.spelling(locale),
)
}
fn ambiguous_enum_spelling_from_args(&mut self, args: &[wir::ValueId]) -> Result<String> {
if args.len() != 2 {
return Err(WorkshopError::malformed(
"ambiguous enum value expects spelling and candidates".to_string(),
None,
));
}
let Some(wir::ValueNode {
value: wir::Value::String(spelling),
..
}) = self.program.values.get(args[0])
else {
return Err(WorkshopError::malformed(
"ambiguous enum spelling must be a string".to_string(),
None,
));
};
let Some(wir::ValueNode {
value: wir::Value::Array(candidate_ids),
..
}) = self.program.values.get(args[1])
else {
return Err(WorkshopError::malformed(
"ambiguous enum candidates must be an array".to_string(),
None,
));
};
let candidates = candidate_ids
.iter()
.map(|candidate_id| {
let Some(wir::ValueNode {
value: wir::Value::Enum { value_type, value },
..
}) = self.program.values.get(*candidate_id)
else {
return Err(WorkshopError::malformed(
"ambiguous enum candidate must be an enum value".to_string(),
None,
));
};
Ok((value_type.clone(), value.clone()))
})
.collect::<Result<Vec<_>>>()?;
self.ambiguous_enum_spelling(spelling, &candidates)
}
fn ambiguous_enum_spelling(
&mut self,
spelling: &str,
candidates: &[(String, String)],
) -> Result<String> {
let common_spelling = |locale: &Locale| {
let members = candidates
.iter()
.filter_map(|(domain, member)| {
self.catalog.enum_domain(domain).and_then(|domain| {
domain
.members
.iter()
.find(|candidate| candidate.member == *member)
})
})
.collect::<Vec<_>>();
if members.is_empty() || members.len() != candidates.len() {
return None;
}
if members.iter().all(|member| {
member
.spellings(locale)
.iter()
.any(|alias| alias == spelling)
}) {
return Some(spelling.to_string());
}
members.first()?.spellings(locale).iter().find_map(|alias| {
members
.iter()
.all(|member| {
member
.spellings(locale)
.iter()
.any(|candidate| candidate == alias)
})
.then(|| alias.to_string())
})
};
self.localized_with(
"ambiguous enum member",
spelling,
format!("enumMember:{spelling}"),
false,
common_spelling,
)
}
pub(crate) fn bare_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
let Some(node) = self.program.values.get(id) else {
return Err(WorkshopError::malformed(
format!("dangling value {id}"),
None,
));
};
if let wir::Value::String(value) = &node.value {
write!(out, "\"{}\"", escape_value_string(value)).unwrap();
return Ok(());
}
self.value(id, out)
}
pub(crate) fn emit_string_value(&mut self, value: &str, out: &mut String) -> Result<()> {
let spelling = self.spelling(Kind::Value, "customString")?;
let segments = split_string(value);
emit_string_chain(spelling, &segments, out);
Ok(())
}
pub(crate) fn value_text(&mut self, id: wir::ValueId) -> Result<String> {
let mut text = String::new();
self.value(id, &mut text)?;
Ok(text)
}
}