use std::collections::HashMap;
use super::stmt_range;
use crate::scope::CollectedScopes;
use crate::*;
use mel_ast::{Expr, Item, ProcDef, ShellWord, Stmt};
use mel_syntax::{SourceView, TextRange};
#[derive(Debug, Clone, PartialEq, Eq)]
enum ValueType {
Int,
Float,
String,
Vector,
Matrix,
Array(Box<ValueType>),
Unknown,
}
struct AssignmentTargetInfo {
name_range: TextRange,
declaration_range: TextRange,
value_type: ValueType,
}
pub(crate) struct Analyzer<'a, R: ?Sized> {
collected: &'a CollectedScopes,
source: SourceView<'a>,
registry: &'a R,
collect_artifacts: bool,
include_warnings: bool,
pub(crate) diagnostics: Vec<Diagnostic>,
pub(crate) invoke_resolutions: Vec<InvokeResolution>,
pub(crate) ident_resolutions: Vec<IdentResolution>,
pub(crate) normalized_invokes: Vec<NormalizedCommandInvoke>,
visible_decl_orders: HashMap<ScopeId, usize>,
visible_variable_decl_orders: HashMap<ScopeId, usize>,
implicit_variables_by_scope: HashMap<ScopeId, Vec<TextRange>>,
proc_contexts: Vec<ProcContext>,
}
struct ProcContext {
name_range: TextRange,
range: TextRange,
return_type: Option<ValueType>,
saw_value_return: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ResolvedCommand<'a> {
schema: &'a ValidatedCommandSchema,
}
enum ResolvedInvokeTarget<'a> {
Proc {
symbol_id: ProcSymbolId,
forward_reference: bool,
},
Command(ResolvedCommand<'a>),
Unresolved,
}
impl ResolvedInvokeTarget<'_> {
fn proc_symbol(&self) -> Option<ProcSymbolId> {
match self {
Self::Proc { symbol_id, .. } => Some(*symbol_id),
Self::Command(_) | Self::Unresolved => None,
}
}
fn into_callee_resolution(self) -> ResolvedCallee {
match self {
Self::Proc { symbol_id, .. } => ResolvedCallee::Proc(symbol_id),
Self::Command(command) => match command.kind() {
CommandKind::Builtin => ResolvedCallee::BuiltinCommand(command.schema.name.clone()),
CommandKind::Plugin => ResolvedCallee::PluginCommand(command.schema.name.clone()),
},
Self::Unresolved => ResolvedCallee::Unresolved,
}
}
fn is_forward_reference(&self) -> bool {
matches!(
self,
Self::Proc {
forward_reference: true,
..
}
)
}
}
impl ResolvedCommand<'_> {
fn kind(&self) -> CommandKind {
self.schema.kind
}
}
impl<'a, R> Analyzer<'a, R>
where
R: CommandRegistry + ?Sized,
{
pub(crate) fn new(
collected: &'a CollectedScopes,
source: SourceView<'a>,
registry: &'a R,
collect_artifacts: bool,
filter: DiagnosticFilter,
) -> Self {
Self {
collected,
source,
registry,
collect_artifacts,
include_warnings: matches!(filter, DiagnosticFilter::All),
diagnostics: Vec::new(),
invoke_resolutions: Vec::new(),
ident_resolutions: Vec::new(),
normalized_invokes: Vec::new(),
visible_decl_orders: HashMap::new(),
visible_variable_decl_orders: HashMap::new(),
implicit_variables_by_scope: HashMap::new(),
proc_contexts: Vec::new(),
}
}
fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
if diagnostic.severity == DiagnosticSeverity::Warning && !self.include_warnings {
return;
}
self.diagnostics.push(diagnostic);
}
pub(crate) fn walk_item(&mut self, item: &Item, current_scope: ScopeId) {
match item {
Item::Proc(proc_def) => self.walk_proc_def(proc_def, current_scope),
Item::Stmt(stmt) => self.walk_stmt(stmt, current_scope),
}
}
fn walk_proc_def(&mut self, proc_def: &ProcDef, current_scope: ScopeId) {
self.mark_proc_visible(proc_def);
let body_scope = self.collected.scope_for_stmt(&proc_def.body);
self.mark_proc_params_visible(proc_def);
self.proc_contexts.push(ProcContext {
name_range: proc_def.name_range,
range: proc_def.range,
return_type: proc_def
.return_type
.as_ref()
.map(value_type_from_proc_return_type),
saw_value_return: false,
});
self.walk_stmt_in_existing_scope(&proc_def.body, body_scope);
self.finish_proc_context();
debug_assert_eq!(
self.collected.symbol_for_proc(proc_def).owner_scope,
current_scope
);
}
fn walk_stmt(&mut self, stmt: &Stmt, current_scope: ScopeId) {
match stmt {
Stmt::Empty { .. } | Stmt::Break { .. } | Stmt::Continue { .. } => {}
Stmt::Block { .. } => {
let block_scope = self.collected.scope_for_stmt(stmt);
self.walk_stmt_in_existing_scope(stmt, block_scope);
}
Stmt::Proc { proc_def, .. } => {
self.walk_proc_def(proc_def, current_scope);
}
Stmt::Expr { expr, .. } => self.walk_expr(expr, current_scope),
Stmt::VarDecl { decl, .. } => {
for declarator in &decl.declarators {
if let Some(Some(size)) = &declarator.array_size {
self.walk_expr(size, current_scope);
}
if let Some(initializer) = &declarator.initializer {
self.walk_expr(initializer, current_scope);
self.validate_var_initializer(decl, declarator, initializer, current_scope);
}
}
self.mark_stmt_variables_visible(stmt);
}
Stmt::If {
condition,
then_branch,
else_branch,
..
} => {
self.walk_expr(condition, current_scope);
self.walk_stmt_in_child_scope(then_branch);
if let Some(else_branch) = else_branch {
self.walk_stmt_in_child_scope(else_branch);
}
}
Stmt::While {
condition, body, ..
} => {
self.walk_expr(condition, current_scope);
self.walk_stmt_in_child_scope(body);
}
Stmt::DoWhile {
body, condition, ..
} => {
self.walk_stmt_in_child_scope(body);
self.walk_expr(condition, current_scope);
}
Stmt::Switch {
control, clauses, ..
} => {
self.walk_expr(control, current_scope);
for clause in clauses {
if let mel_ast::SwitchLabel::Case(expr) = &clause.label {
self.walk_expr(expr, current_scope);
}
let clause_scope = self.collected.scope_for_clause(clause);
for stmt in &clause.statements {
self.walk_stmt(stmt, clause_scope);
}
}
}
Stmt::For {
init,
condition,
update,
body,
..
} => {
if let Some(init) = init {
for expr in init {
self.walk_expr(expr, current_scope);
}
}
if let Some(condition) = condition {
self.walk_expr(condition, current_scope);
}
if let Some(update) = update {
for expr in update {
self.walk_expr(expr, current_scope);
}
}
self.walk_stmt_in_child_scope(body);
}
Stmt::ForIn {
binding,
iterable,
body,
..
} => {
self.walk_expr(iterable, current_scope);
self.walk_assign_target(binding, current_scope, false);
self.walk_stmt_in_child_scope(body);
}
Stmt::Return { expr, .. } => {
self.validate_return_stmt(expr.as_ref(), stmt_range(stmt), current_scope);
if let Some(expr) = expr {
self.walk_expr(expr, current_scope);
}
}
}
}
fn walk_stmt_in_child_scope(&mut self, stmt: &Stmt) {
let child_scope = self.collected.scope_for_stmt(stmt);
self.walk_stmt_in_existing_scope(stmt, child_scope);
}
fn walk_stmt_in_existing_scope(&mut self, stmt: &Stmt, current_scope: ScopeId) {
match stmt {
Stmt::Block { statements, .. } => {
for stmt in statements {
self.walk_stmt(stmt, current_scope);
}
}
_ => self.walk_stmt(stmt, current_scope),
}
}
fn walk_expr(&mut self, expr: &Expr, current_scope: ScopeId) {
match expr {
Expr::Cast { expr, .. } => self.walk_expr(expr, current_scope),
Expr::Unary { expr, .. } => self.walk_expr(expr, current_scope),
Expr::PrefixUpdate { expr, .. } | Expr::PostfixUpdate { expr, .. } => {
self.walk_assign_target(expr, current_scope, true);
}
Expr::Binary { lhs, rhs, .. } => {
self.walk_expr(lhs, current_scope);
self.walk_expr(rhs, current_scope);
}
Expr::Assign { op, lhs, rhs, .. } => {
self.walk_expr(rhs, current_scope);
self.validate_assignment_expr(op, lhs, rhs, current_scope);
self.walk_assign_target(
lhs,
current_scope,
!matches!(op, mel_ast::AssignOp::Assign),
);
}
Expr::Ternary {
condition,
then_expr,
else_expr,
..
} => {
self.walk_expr(condition, current_scope);
self.walk_expr(then_expr, current_scope);
self.walk_expr(else_expr, current_scope);
}
Expr::Index { target, index, .. } => {
self.walk_expr(target, current_scope);
self.walk_expr(index, current_scope);
}
Expr::MemberAccess { target, .. } | Expr::ComponentAccess { target, .. } => {
self.walk_expr(target, current_scope);
}
Expr::VectorLiteral { elements, .. } | Expr::ArrayLiteral { elements, .. } => {
for element in elements {
self.walk_expr(element, current_scope);
}
}
Expr::Invoke(invoke) => self.walk_invoke(invoke, current_scope),
Expr::Ident { name_range, range } => {
let name = self.slice(*name_range);
let resolution = self.resolve_ident(name, current_scope);
if matches!(resolution, IdentTarget::Unresolved)
&& !is_boolean_alias(name)
&& !self.is_visible_implicit_variable(name, current_scope)
{
self.push_diagnostic(Diagnostic::warning(
format!("unresolved variable \"{name}\""),
*range,
));
}
if self.collect_artifacts {
self.ident_resolutions.push(IdentResolution {
range: *range,
scope: current_scope,
name_range: *name_range,
resolution,
});
}
}
Expr::BareWord { .. } | Expr::Int { .. } | Expr::Float { .. } | Expr::String { .. } => {
}
}
}
fn walk_assign_target(&mut self, expr: &Expr, current_scope: ScopeId, emit_unresolved: bool) {
match expr {
Expr::Ident { name_range, range } => {
let name = self.slice(*name_range);
let resolution = self.resolve_ident(name, current_scope);
if emit_unresolved
&& matches!(resolution, IdentTarget::Unresolved)
&& !self.is_visible_implicit_variable(name, current_scope)
{
self.push_diagnostic(Diagnostic::warning(
format!("unresolved variable \"{name}\""),
*range,
));
}
if !emit_unresolved && matches!(resolution, IdentTarget::Unresolved) {
self.mark_implicit_variable(*name_range, current_scope);
}
if self.collect_artifacts {
self.ident_resolutions.push(IdentResolution {
range: *range,
scope: current_scope,
name_range: *name_range,
resolution,
});
}
}
Expr::Index { target, index, .. } => {
self.walk_expr(target, current_scope);
self.walk_expr(index, current_scope);
}
Expr::MemberAccess { target, .. } | Expr::ComponentAccess { target, .. } => {
self.walk_expr(target, current_scope);
}
_ => self.walk_expr(expr, current_scope),
}
}
fn walk_invoke(&mut self, invoke: &mel_ast::InvokeExpr, current_scope: ScopeId) {
let resolution = match &invoke.surface {
mel_ast::InvokeSurface::Function { head_range, args } => {
for arg in args {
self.walk_expr(arg, current_scope);
}
let name = self.source.slice(*head_range);
let resolved = resolve_named_target(
self.collected,
self.source,
self.registry,
&self.visible_decl_orders,
name,
current_scope,
);
if resolved.is_forward_reference() {
self.diagnostics.push(Diagnostic::error(
format!("local proc \"{name}\" is called before its definition"),
invoke.range,
));
}
self.validate_proc_arity(resolved.proc_symbol(), args.len(), invoke.range);
resolved.into_callee_resolution()
}
mel_ast::InvokeSurface::ShellLike {
head_range, words, ..
} => {
for word in words {
self.walk_shell_word(word, current_scope);
}
let name = self.source.slice(*head_range);
let resolved = resolve_named_target(
self.collected,
self.source,
self.registry,
&self.visible_decl_orders,
name,
current_scope,
);
if resolved.is_forward_reference() {
self.diagnostics.push(Diagnostic::error(
format!("local proc \"{name}\" is called before its definition"),
invoke.range,
));
}
if let ResolvedInvokeTarget::Command(ref command) = resolved {
let schema = command.schema;
if self.collect_artifacts {
let (normalized, diagnostics) = command_norm::normalize_shell_like_invoke(
schema,
current_scope,
*head_range,
words,
invoke.range,
self.source,
);
self.diagnostics.extend(diagnostics);
self.normalized_invokes.push(normalized);
} else {
self.diagnostics
.extend(command_norm::collect_command_diagnostics(
schema,
words,
invoke.range,
self.source,
self.include_warnings,
));
}
};
resolved.into_callee_resolution()
}
};
if self.collect_artifacts {
self.invoke_resolutions.push(InvokeResolution {
range: invoke.range,
scope: current_scope,
resolution,
});
}
}
fn validate_proc_arity(
&mut self,
proc_symbol: Option<ProcSymbolId>,
actual_args: usize,
call_range: TextRange,
) {
let Some(proc_symbol) = proc_symbol else {
return;
};
let symbol = self.collected.symbol(proc_symbol);
let expected_args = self
.collected
.param_symbols_for_proc_range(symbol.range)
.len();
if actual_args == expected_args {
return;
}
let proc_name = self.collected.proc_name(self.source, proc_symbol);
self.diagnostics.push(
Diagnostic::error(
format!(
"proc \"{proc_name}\" expects {expected_args} argument(s) but call provides {actual_args}"
),
call_range,
)
.with_secondary_label("proc defined here", symbol.name_range),
);
}
fn walk_shell_word(&mut self, word: &ShellWord, current_scope: ScopeId) {
match word {
ShellWord::Flag { .. }
| ShellWord::NumericLiteral { .. }
| ShellWord::BareWord { .. }
| ShellWord::QuotedString { .. } => {}
ShellWord::Variable { expr, .. }
| ShellWord::GroupedExpr { expr, .. }
| ShellWord::BraceList { expr, .. }
| ShellWord::VectorLiteral { expr, .. } => self.walk_expr(expr, current_scope),
ShellWord::Capture { invoke, .. } => self.walk_invoke(invoke, current_scope),
}
}
fn mark_proc_visible(&mut self, proc_def: &ProcDef) {
let symbol = self.collected.symbol_for_proc(proc_def);
if symbol.is_global {
return;
}
let visible_order = self
.visible_decl_orders
.entry(symbol.owner_scope)
.or_insert(0);
*visible_order = (*visible_order).max(symbol.decl_order);
}
fn mark_proc_params_visible(&mut self, proc_def: &ProcDef) {
for param_id in self.collected.param_symbols_for_proc(proc_def) {
let symbol = self.collected.variable_symbol(*param_id);
let visible_order = self
.visible_variable_decl_orders
.entry(symbol.owner_scope)
.or_insert(0);
*visible_order = (*visible_order).max(symbol.decl_order);
}
}
fn mark_stmt_variables_visible(&mut self, stmt: &Stmt) {
for variable_id in self.collected.variable_symbols_for_stmt(stmt) {
let symbol = self.collected.variable_symbol(*variable_id);
if matches!(symbol.kind, VariableKind::Global) {
continue;
}
let visible_order = self
.visible_variable_decl_orders
.entry(symbol.owner_scope)
.or_insert(0);
*visible_order = (*visible_order).max(symbol.decl_order);
}
}
fn resolve_ident(&self, name: &str, current_scope: ScopeId) -> IdentTarget {
if let Some(symbol) = self.collected.find_visible_local_variable(
self.source,
name,
current_scope,
&self.visible_variable_decl_orders,
) {
return IdentTarget::Variable(symbol.id);
}
if let Some(symbol) = self.collected.find_global_variable(self.source, name) {
return IdentTarget::Variable(symbol.id);
}
IdentTarget::Unresolved
}
fn mark_implicit_variable(&mut self, name_range: TextRange, current_scope: ScopeId) {
let source = self.source;
let name = source.slice(name_range);
let names = self
.implicit_variables_by_scope
.entry(current_scope)
.or_default();
if !names
.iter()
.any(|candidate| source.slice(*candidate) == name)
{
names.push(name_range);
}
}
fn is_visible_implicit_variable(&self, name: &str, current_scope: ScopeId) -> bool {
let mut scope = Some(current_scope);
while let Some(scope_id) = scope {
if self
.implicit_variables_by_scope
.get(&scope_id)
.is_some_and(|names| names.iter().any(|candidate| self.slice(*candidate) == name))
{
return true;
}
scope = self.collected.scopes.parent(scope_id);
}
false
}
fn validate_return_stmt(
&mut self,
expr: Option<&Expr>,
range: TextRange,
current_scope: ScopeId,
) {
let actual = expr.map(|expr| self.infer_expr_type(expr, current_scope));
let Some(context) = self.proc_contexts.last_mut() else {
return;
};
let context_name = self.source.slice(context.name_range);
match (&context.return_type, actual.as_ref()) {
(None, Some(_)) => self.diagnostics.push(Diagnostic::error(
format!(
"proc \"{}\" has no return type but returns a value",
context_name
),
range,
)),
(Some(expected), Some(actual)) => {
context.saw_value_return = true;
if !is_assignable(expected, actual) {
let message = format!(
"proc \"{}\" returns {:?} but declares {:?}",
context_name, actual, expected
);
let diagnostic = if is_scalar_coercion_warning(expected, actual) {
Diagnostic::warning(message, range)
} else {
Diagnostic::error(message, range)
};
self.push_diagnostic(diagnostic);
}
}
(Some(_), None) | (None, None) => {}
}
}
fn finish_proc_context(&mut self) {
let Some(context) = self.proc_contexts.pop() else {
return;
};
if context.return_type.is_some() && !context.saw_value_return {
self.diagnostics.push(Diagnostic::error(
format!(
"proc \"{}\" declares a return type but never returns a value",
self.slice(context.name_range)
),
context.range,
));
}
}
fn validate_var_initializer(
&mut self,
decl: &mel_ast::VarDecl,
declarator: &mel_ast::Declarator,
initializer: &Expr,
current_scope: ScopeId,
) {
let expected = value_type_from_var_decl(decl, declarator);
let actual = self.infer_expr_type(initializer, current_scope);
if !is_assignable(&expected, &actual) {
let message = format!(
"variable \"{}\" has declared type {:?} but initializer is {:?}",
self.slice(declarator.name_range),
expected,
actual
);
let diagnostic = if is_scalar_coercion_warning(&expected, &actual) {
Diagnostic::warning(message, initializer.range())
} else {
Diagnostic::error(message, initializer.range())
};
self.push_diagnostic(diagnostic);
}
}
fn validate_assignment_expr(
&mut self,
op: &mel_ast::AssignOp,
lhs: &Expr,
rhs: &Expr,
current_scope: ScopeId,
) {
let Some(target_info) = self.infer_assignment_target_info(lhs, current_scope) else {
return;
};
let rhs_ty = self.infer_expr_type(rhs, current_scope);
if matches!(rhs_ty, ValueType::Unknown) {
return;
}
let actual = match op {
mel_ast::AssignOp::Assign => rhs_ty,
mel_ast::AssignOp::AddAssign
| mel_ast::AssignOp::SubAssign
| mel_ast::AssignOp::MulAssign
| mel_ast::AssignOp::DivAssign => {
let combined =
combine_numeric_types(target_info.value_type.clone(), rhs_ty.clone());
if matches!(combined, ValueType::Unknown) {
rhs_ty
} else {
combined
}
}
};
if !is_assignable(&target_info.value_type, &actual) {
let message = format!(
"variable \"{}\" has declared type {:?} but assigned expression is {:?}",
self.slice(target_info.name_range),
target_info.value_type,
actual
);
let diagnostic = if is_scalar_coercion_warning(&target_info.value_type, &actual) {
Diagnostic::warning(message, rhs.range())
} else {
Diagnostic::error(message, rhs.range())
};
self.diagnostics.push(diagnostic.with_secondary_label(
format!(
"\"{}\" declared here with type {:?}",
self.slice(target_info.name_range),
target_info.value_type
),
target_info.declaration_range,
));
}
}
fn infer_expr_type(&self, expr: &Expr, current_scope: ScopeId) -> ValueType {
match expr {
Expr::Ident { name_range, .. } => {
self.infer_ident_type(self.slice(*name_range), current_scope)
}
Expr::Int { .. } => ValueType::Int,
Expr::Float { .. } => ValueType::Float,
Expr::String { .. } | Expr::BareWord { .. } => ValueType::String,
Expr::Cast { ty, .. } => value_type_from_type_name(ty),
Expr::VectorLiteral { .. } => ValueType::Vector,
Expr::ArrayLiteral { elements, .. } => {
infer_array_literal_type(elements, self, current_scope)
}
Expr::Unary { op, expr, .. } => self.infer_unary_type(op, expr, current_scope),
Expr::PrefixUpdate { expr, .. }
| Expr::PostfixUpdate { expr, .. }
| Expr::ComponentAccess { target: expr, .. } => {
self.infer_expr_type(expr, current_scope)
}
Expr::MemberAccess { target, .. } => {
let _ = self.infer_expr_type(target, current_scope);
ValueType::Unknown
}
Expr::Binary { op, lhs, rhs, .. } => {
self.infer_binary_type(op, lhs, rhs, current_scope)
}
Expr::Assign { lhs, rhs, .. } => combine_numeric_types(
self.infer_expr_type(lhs, current_scope),
self.infer_expr_type(rhs, current_scope),
),
Expr::Ternary {
then_expr,
else_expr,
..
} => {
let then_ty = self.infer_expr_type(then_expr, current_scope);
let else_ty = self.infer_expr_type(else_expr, current_scope);
if is_assignable(&then_ty, &else_ty) {
then_ty
} else if is_assignable(&else_ty, &then_ty) {
else_ty
} else {
ValueType::Unknown
}
}
Expr::Index { target, .. } => match self.infer_expr_type(target, current_scope) {
ValueType::Array(inner) => *inner,
_ => ValueType::Unknown,
},
Expr::Invoke(invoke) => self.infer_invoke_type(invoke, current_scope),
}
}
fn infer_assignment_target_info(
&self,
expr: &Expr,
current_scope: ScopeId,
) -> Option<AssignmentTargetInfo> {
match expr {
Expr::Ident { name_range, .. } => {
let name = self.slice(*name_range);
match self.resolve_ident(name, current_scope) {
IdentTarget::Variable(symbol_id) => {
let symbol = self.collected.variable_symbol(symbol_id);
let base = value_type_from_type_name(&symbol.ty);
Some(AssignmentTargetInfo {
name_range: symbol.name_range,
declaration_range: symbol.name_range,
value_type: if symbol.is_array {
ValueType::Array(Box::new(base))
} else {
base
},
})
}
IdentTarget::Unresolved => None,
}
}
Expr::Index { target, .. } => {
match self.infer_assignment_target_info(target, current_scope) {
Some(AssignmentTargetInfo {
name_range,
declaration_range,
value_type: ValueType::Array(inner),
}) => Some(AssignmentTargetInfo {
name_range,
declaration_range,
value_type: *inner,
}),
_ => None,
}
}
Expr::MemberAccess { .. } | Expr::ComponentAccess { .. } => None,
_ => None,
}
}
fn infer_ident_type(&self, name: &str, current_scope: ScopeId) -> ValueType {
if is_boolean_alias(name) {
return ValueType::Int;
}
match self.resolve_ident(name, current_scope) {
IdentTarget::Unresolved => ValueType::Unknown,
IdentTarget::Variable(symbol_id) => {
let symbol = self.collected.variable_symbol(symbol_id);
let base = value_type_from_type_name(&symbol.ty);
if symbol.is_array {
ValueType::Array(Box::new(base))
} else {
base
}
}
}
}
fn infer_unary_type(
&self,
op: &mel_ast::UnaryOp,
expr: &Expr,
current_scope: ScopeId,
) -> ValueType {
match op {
mel_ast::UnaryOp::Not => {
let _ = self.infer_expr_type(expr, current_scope);
ValueType::Int
}
mel_ast::UnaryOp::Negate => self.infer_expr_type(expr, current_scope),
}
}
fn infer_binary_type(
&self,
op: &mel_ast::BinaryOp,
lhs: &Expr,
rhs: &Expr,
current_scope: ScopeId,
) -> ValueType {
let lhs = self.infer_expr_type(lhs, current_scope);
let rhs = self.infer_expr_type(rhs, current_scope);
match op {
mel_ast::BinaryOp::Mul
| mel_ast::BinaryOp::Div
| mel_ast::BinaryOp::Rem
| mel_ast::BinaryOp::Caret
| mel_ast::BinaryOp::Add
| mel_ast::BinaryOp::Sub => combine_numeric_types(lhs, rhs),
mel_ast::BinaryOp::Lt
| mel_ast::BinaryOp::Le
| mel_ast::BinaryOp::Gt
| mel_ast::BinaryOp::Ge
| mel_ast::BinaryOp::EqEq
| mel_ast::BinaryOp::NotEq
| mel_ast::BinaryOp::AndAnd
| mel_ast::BinaryOp::OrOr => ValueType::Int,
}
}
fn infer_invoke_type(&self, invoke: &mel_ast::InvokeExpr, current_scope: ScopeId) -> ValueType {
let name = match &invoke.surface {
mel_ast::InvokeSurface::Function { head_range, .. }
| mel_ast::InvokeSurface::ShellLike { head_range, .. } => self.slice(*head_range),
};
let Some(symbol) = self.collected.find_resolved_proc_symbol(
self.source,
name,
current_scope,
&self.visible_decl_orders,
) else {
return ValueType::Unknown;
};
symbol
.return_type
.as_ref()
.map(value_type_from_proc_return_type)
.unwrap_or(ValueType::Unknown)
}
fn slice(&self, range: TextRange) -> &str {
self.source.slice(range)
}
}
fn resolve_named_target<'a, R>(
collected: &'a CollectedScopes,
source: SourceView<'a>,
registry: &'a R,
visible_decl_orders: &HashMap<ScopeId, usize>,
name: &str,
current_scope: ScopeId,
) -> ResolvedInvokeTarget<'a>
where
R: CommandRegistry + ?Sized,
{
if let Some(symbol) =
collected.find_visible_local_proc(source, name, current_scope, visible_decl_orders)
{
return ResolvedInvokeTarget::Proc {
symbol_id: symbol.id,
forward_reference: false,
};
}
if let Some(symbol) =
collected.find_forward_local_proc(source, name, current_scope, visible_decl_orders)
{
return ResolvedInvokeTarget::Proc {
symbol_id: symbol.id,
forward_reference: true,
};
}
if let Some(symbol) = collected.find_global_proc(source, name) {
return ResolvedInvokeTarget::Proc {
symbol_id: symbol.id,
forward_reference: false,
};
}
if let Some(command) = registry.lookup(name) {
return ResolvedInvokeTarget::Command(ResolvedCommand { schema: command });
}
ResolvedInvokeTarget::Unresolved
}
fn value_type_from_type_name(ty: &mel_ast::TypeName) -> ValueType {
match ty {
mel_ast::TypeName::Int => ValueType::Int,
mel_ast::TypeName::Float => ValueType::Float,
mel_ast::TypeName::String => ValueType::String,
mel_ast::TypeName::Vector => ValueType::Vector,
mel_ast::TypeName::Matrix => ValueType::Matrix,
}
}
fn value_type_from_proc_return_type(return_type: &mel_ast::ProcReturnType) -> ValueType {
let base = value_type_from_type_name(&return_type.ty);
if return_type.is_array {
ValueType::Array(Box::new(base))
} else {
base
}
}
fn value_type_from_var_decl(
decl: &mel_ast::VarDecl,
declarator: &mel_ast::Declarator,
) -> ValueType {
let base = value_type_from_type_name(&decl.ty);
if declarator.array_size.is_some() {
ValueType::Array(Box::new(base))
} else {
base
}
}
fn is_assignable(expected: &ValueType, actual: &ValueType) -> bool {
match (expected, actual) {
(_, ValueType::Unknown) | (ValueType::Unknown, _) => true,
(ValueType::Float, ValueType::Int) => true,
(ValueType::Array(expected), ValueType::Array(actual)) => is_assignable(expected, actual),
_ => expected == actual,
}
}
fn is_scalar_coercion_warning(expected: &ValueType, actual: &ValueType) -> bool {
matches!(
(expected, actual),
(ValueType::Int, ValueType::Float | ValueType::String)
| (ValueType::Float, ValueType::Int | ValueType::String)
| (ValueType::String, ValueType::Int | ValueType::Float)
)
}
fn combine_numeric_types(lhs: ValueType, rhs: ValueType) -> ValueType {
match (&lhs, &rhs) {
(ValueType::Float, ValueType::Int)
| (ValueType::Int, ValueType::Float)
| (ValueType::Float, ValueType::Float) => ValueType::Float,
(ValueType::Int, ValueType::Int) => ValueType::Int,
_ if lhs == rhs => lhs,
_ => ValueType::Unknown,
}
}
fn is_boolean_alias(name: &str) -> bool {
matches!(name, "true" | "false" | "on" | "off")
}
fn infer_array_literal_type<R>(
elements: &[Expr],
analyzer: &Analyzer<'_, R>,
current_scope: ScopeId,
) -> ValueType
where
R: CommandRegistry + ?Sized,
{
let mut iter = elements.iter();
let Some(first) = iter.next() else {
return ValueType::Unknown;
};
let first_ty = analyzer.infer_expr_type(first, current_scope);
if iter.all(|expr| is_assignable(&first_ty, &analyzer.infer_expr_type(expr, current_scope))) {
ValueType::Array(Box::new(first_ty))
} else {
ValueType::Unknown
}
}