use crate::errors::SourceLocation;
use crate::parser::ast::*;
use super::Analyzer;
fn body_returns_a_value(body: &[Statement]) -> bool {
body.iter().any(statement_returns_a_value)
}
fn statement_returns_a_value(stmt: &Statement) -> bool {
match stmt {
Statement::Return { value: Some(_), .. } => true,
Statement::If { then_block, else_if_blocks, else_block, .. } => {
body_returns_a_value(then_block)
|| else_if_blocks.iter().any(|(_, blk)| body_returns_a_value(blk))
|| else_block.as_ref().is_some_and(|blk| body_returns_a_value(blk))
}
Statement::While { body, .. }
| Statement::ForRange { body, .. }
| Statement::ForEach { body, .. }
| Statement::Repeat { body, .. } => body_returns_a_value(body),
_ => false,
}
}
#[derive(Clone, Copy)]
enum VoidResult {
Procedure,
LibraryEntry,
}
impl Analyzer {
pub(crate) fn record_procedure(
&mut self,
key: String,
return_type: &Type,
body: &[Statement],
) {
if *return_type == Type::Void && !body_returns_a_value(body) {
self.procedures.insert(key);
}
}
fn void_result_of(&self, name: &str) -> Option<VoidResult> {
if self.functions.contains(&self.func_key(name)) {
return self
.procedures
.contains(&self.func_key(name))
.then_some(VoidResult::Procedure);
}
match self.imported_providers(name).as_slice() {
[only] if only.return_type == Type::Void => Some(VoidResult::LibraryEntry),
_ => None,
}
}
pub(crate) fn reject_void_call_result(&mut self, name: &str) {
let Some(kind) = self.void_result_of(name) else {
return;
};
let (message, hint) = match kind {
VoidResult::Procedure => (
format!(
"'{}' returns nothing, so its result cannot be used as a value here\n \
A `To` with no `Return` hands nothing back (LANGUAGE.md \
\"Functions\"), and this position reads a value - so what lands \
here is whatever the call left in the return register, not an \
answer.",
name
),
format!(
"give '{}' a `Return a <type>, <expression>.`, or call '{}' as a \
statement instead of using its result",
name, name
),
),
VoidResult::LibraryEntry => (
format!(
"'{}' has no declared return type in its .lib entry, so its result \
cannot be used as a value here\n \
A `.lib` entry with no `, returning` clause is a function that \
returns nothing (LANGUAGE.md:4963-4965), and consuming a library \
type-checks its calls like any other function's \
(LANGUAGE.md:4990) - so what lands here is whatever the call left \
in the return register, not an answer.",
name
),
format!(
"add `, returning a <type>` to {}'s .lib entry, or call '{}' as a \
statement instead of using its result",
name, name
),
),
};
match self.use_site_location(name) {
Some(loc) => self.push_error_with_hint_at(message, Some(loc), Some(&hint)),
None => self.push_error_with_hint(message, Some(name), Some(&hint)),
}
}
fn use_site_location(&mut self, name: &str) -> Option<SourceLocation> {
let definition_line = self.definition_line(name);
let patterns = [
format!("{{{}", name),
format!("'{}'", name),
name.to_string(),
];
let occurrence = *self.symbol_error_counts.get(name).unwrap_or(&0);
let found = self.find_pattern_location(name, &patterns, occurrence, definition_line, false);
self.symbol_error_counts.insert(name.to_string(), occurrence + 1);
found
}
fn definition_line(&self, name: &str) -> Option<usize> {
let source = self.source_file.as_ref()?;
let quoted = format!("To '{}'", name);
let bare = format!("To {}", name);
source
.content
.lines()
.position(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("ed)
|| (trimmed.starts_with(&bare)
&& trimmed[bare.len()..]
.chars()
.next()
.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_'))
})
.map(|index| index + 1)
}
}