use yog_ids::script_problem;
use super::Checker;
use super::context::Context;
use super::game::{self, Found};
use super::model::Type;
use super::own::TypeKey;
use crate::ast::{Expr, ExprKind, Method, Pass};
use crate::source::token::Span;
pub(super) enum Callee {
Own(Vec<(Method, Context)>),
Extension(Vec<(Method, Context)>),
Typed(Vec<Signature>),
Unknown,
}
pub(super) struct Signature {
pub params: Vec<Type>,
pub passes: Vec<Option<Pass>>,
pub required: usize,
pub ret: Type,
pub generics: Vec<String>,
pub bound: super::generics::Bound,
pub rest: bool,
}
impl Checker<'_, '_, '_> {
pub(super) fn member(&mut self, on: &Type, name: &str, safe: bool, at: Span) -> Type {
if is_side_call(name) {
return Type::Unknown;
}
if let Some(t) = self.nullable_member(on, name) {
return t;
}
if on.is_nullable() && !safe {
self.report.error(
script_problem::NULLABLE,
at,
format!("this may be `null`: `?.{name}` reads `{name}` only when it isn't"),
);
}
let t = match on.non_null() {
Type::Tuple(types, names) => match Self::tuple_item(types, names, name) {
Some(t) => t,
None => {
self.unknown_member(on.non_null(), name, at);
Type::Unknown
}
},
Type::Static(of, args) => {
let t = self.statics_type(of, args);
let found = self.own_member(&t, name);
let case = self
.own
.named(of)
.is_some_and(|(_, o)| o.cases.iter().any(|c| c == name));
if !found.known() && !case && !self.own_has_method(&t, name) {
self.unknown_member(on.non_null(), name, at);
}
self.static_field_read(of, name, &found, at);
found
}
Type::Own(..) => {
let t = self.own_member(on.non_null(), name);
if !t.known() && !self.own_has_method(on.non_null(), name) {
self.unknown_member(on.non_null(), name, at);
}
t
}
Type::Game(class) => match self.extension_value(class, name) {
Some(t) => t,
None => self.game_value(class, name, at),
},
Type::Namespace(ns) => match self.in_namespace(&super::names::split(ns), name, at) {
Type::Own(t, args) => Type::Static(t, args),
other => other,
},
Type::Param(p) => match self.param_member(p, name) {
Some((_, t)) => t,
None => self.object_member(on.non_null(), name, at),
},
Type::Yog(type_name) => match self.extension_value(type_name, name) {
Some(t) => t,
None => self.yog_value(type_name, name, at),
},
other => match self.hosted(other) {
Some(host) => {
let t = self.own_member(&host, name);
if !t.known() && !self.own_has_method(&host, name) {
self.unknown_member(other, name, at);
}
t
}
None => Type::Unknown,
},
};
if safe { t.nullable() } else { t }
}
pub(super) fn own_member(&mut self, t: &Type, name: &str) -> Type {
let (type_name, args) = match t {
Type::Own(n, args) => (n, args),
Type::Game(class) | Type::Yog(class) => {
return self.extension_value(class, name).unwrap_or(Type::Unknown);
}
_ => return Type::Unknown,
};
for (key, args) in self.member_lineage(type_name, args) {
let context = self.context_of(&key.name, &args);
let found = self.in_context(&context, |c| c.own_member_of(&key, name));
if found.known() {
return found;
}
}
Type::Unknown
}
fn own_member_of(&mut self, key: &TypeKey, name: &str) -> Type {
let Some(own) = self.own.of_key(key) else {
return Type::Unknown;
};
if let Some(written) = own.values.get(name).cloned() {
return self.type_of(&written);
}
if own.cases.iter().any(|c| c == name) {
let args = own
.generics
.clone()
.iter()
.map(|g| match self.type_param(g) {
Some(Type::Param(_)) | None => Type::Unknown,
Some(t) => t,
})
.collect();
return Type::Own(key.name.clone(), args);
}
let components = own.components.clone();
if components.iter().any(|c| c == name) {
return Type::own(name.to_string());
}
components
.iter()
.map(|c| self.own_member(&Type::own(c.clone()), name))
.find(Type::known)
.unwrap_or(Type::Unknown)
}
fn extension_value(&mut self, class: &str, name: &str) -> Option<Type> {
let ext = self.own.extended.get(crate::index::simple_name(class))?;
if let Some(written) = ext.values.get(name).cloned() {
return Some(self.type_of(&written));
}
let components = ext.components.clone();
if components.iter().any(|c| c == name) {
return Some(Type::own(name.to_string()));
}
components
.iter()
.map(|c| self.own_member(&Type::own(c.clone()), name))
.find(Type::known)
}
pub(super) fn extension_methods(
&self,
class: &str,
name: &str,
) -> Option<Vec<(Method, Context)>> {
let ext = self.own.extended.get(crate::index::simple_name(class))?;
match ext.methods.get(name) {
Some(methods) => Some(self.unbound(methods)),
None => ext
.components
.iter()
.find_map(|c| self.own_methods(&Type::own(c.clone()), name)),
}
}
pub(super) fn own_has_method(&self, t: &Type, name: &str) -> bool {
self.own_methods(t, name).is_some()
}
fn game_value(&mut self, class: &str, name: &str, at: Span) -> Type {
let Some(lookup) = &self.lookup else {
return Type::Unknown;
};
let Some(c) = lookup.class(class) else {
return Type::Unknown;
};
match game::member(lookup, c, name, false) {
Found::Value(t) => t,
Found::Nothing if matches!(game::member(lookup, c, name, true), Found::Nothing) => {
self.unknown_member(&Type::Game(class.to_string()), name, at);
Type::Unknown
}
_ => Type::Unknown,
}
}
fn yog_value(&mut self, type_name: &str, name: &str, at: Span) -> Type {
let surface = &*super::surface::SURFACE;
if let Some(s) = surface.structs.get(type_name) {
if let Some(f) = s
.fields
.iter()
.find(|f| yog_codegen::pascal(f.name) == name)
{
return Type::of_shape(&(f.shape)());
}
} else if let Some(e) = surface.enums.get(type_name) {
if e.variants
.iter()
.any(|v| yog_codegen::pascal(v.name) == name)
{
return Type::Yog(type_name.to_string());
}
} else {
let sub = super::surface::subgroup(type_name, name);
if surface.groups.contains_key(&sub) {
return Type::Yog(sub);
}
if let Some(args) = surface
.groups
.get(type_name)
.and_then(|g| g.events.get(name))
{
return Type::Fn(vec![Type::Yog(args.clone())], Box::new(Type::Void));
}
if surface
.groups
.get(type_name)
.is_some_and(|g| g.methods.contains_key(name))
{
return Type::Unknown;
}
}
self.unknown_member(&Type::Yog(type_name.to_string()), name, at);
Type::Unknown
}
pub(super) fn unknown_member(&mut self, on: &Type, name: &str, at: Span) {
self.report.error(
script_problem::UNKNOWN_MEMBER,
at,
format!("`{on}` has no member `{name}`"),
);
}
fn is_delegate_value(&mut self, e: &Expr) -> bool {
let t = self.quietly(|c| c.expr(e));
self.delegate_sig(&t).is_some()
}
pub(super) fn callee(&mut self, callee: &Expr) -> Callee {
match &callee.kind {
ExprKind::Ident(name) if self.delegate(name).is_some() => {
let t = self.expr(callee);
self.types.set(callee, t);
self.delegate(name)
.map_or(Callee::Unknown, |sig| Callee::Typed(vec![sig]))
}
ExprKind::Member { expr, name, .. }
if name.text == yog_script_ids::clr::INVOKE && self.is_delegate_value(expr) =>
{
let t = self.expr(expr);
match self.delegate_sig(&t) {
Some((params, ret)) => Callee::Typed(vec![signature_of(¶ms, &ret)]),
None => Callee::Unknown,
}
}
ExprKind::Ident(name) if self.local(name).is_none() => {
let methods = self.callable(name);
if methods.is_empty() {
match self.variant_ctor(yog_script_ids::system::RESULT, name) {
Callee::Unknown => self.static_import(name, callee.span),
made => made,
}
} else {
if let Some(Type::Own(of, _)) = self.this.clone()
&& self.own_methods(&Type::own(of.clone()), name).is_some()
{
self.called_on(None, &of, name, callee.span);
self.method_read(&of, name, callee.span);
}
Callee::Own(methods)
}
}
ExprKind::Member { expr, name, .. } => self.method_of(expr, name),
_ => {
self.expr(callee);
Callee::Unknown
}
}
}
pub(super) fn callable(&self, name: &str) -> Vec<(Method, Context)> {
let local: Vec<(Method, Context)> = self
.local_fns
.iter()
.filter(|(_, m)| m.name.text == name)
.map(|(_, m)| (m.clone(), self.here()))
.collect();
if !local.is_empty() {
return local;
}
let in_this = self.this.clone().and_then(|t| match &t {
Type::Game(class) | Type::Yog(class) => self.extension_methods(class, name),
_ => self.own_methods(&t, name),
});
in_this
.or_else(|| self.own.methods.get(name).map(|m| self.unbound(m)))
.unwrap_or_default()
}
pub(super) fn signature_types(&mut self, m: &Method) -> (Vec<Type>, Type) {
let s = self.signature(m);
(s.params, s.ret)
}
pub(super) fn own_methods(&self, t: &Type, name: &str) -> Option<Vec<(Method, Context)>> {
let Type::Own(type_name, args) = t else {
return None;
};
self.member_lineage(type_name, args)
.into_iter()
.find_map(|(key, args)| {
let own = self.own.of_key(&key)?;
match own.methods.get(name) {
Some(methods) => {
let context = self.context_of(&key.name, &args);
Some(
methods
.iter()
.map(|m| (m.clone(), context.clone()))
.collect(),
)
}
None => own
.components
.iter()
.find_map(|c| self.own_methods(&Type::own(c.clone()), name)),
}
})
}
fn delegate(&mut self, name: &str) -> Option<Signature> {
let t = match self.local(name) {
Some(t) => t.clone(),
None => self.this.clone().map(|this| self.own_member(&this, name))?,
};
self.delegate_sig(&t)
.map(|(params, ret)| signature_of(¶ms, &ret))
}
fn static_import(&mut self, name: &str, at: Span) -> Callee {
let statics = self.directives.statics.clone();
for path in statics {
let names: Vec<crate::ast::Name> = path
.iter()
.map(|text| crate::ast::Name {
text: text.clone(),
span: at,
})
.collect();
if let Type::Yog(group) = self.qualified(&names, at)
&& super::surface::SURFACE
.groups
.get(&group)
.is_some_and(|g| g.methods.contains_key(name))
{
return self.yog_methods(&group, name, at);
}
}
let game_this = matches!(self.this, Some(Type::Game(_)));
if !(self.nanites || game_this) {
self.report.error(
script_problem::UNKNOWN_MEMBER,
at,
format!("no method `{name}` here (another file's are its own: a type's are reached through it)"),
);
}
Callee::Unknown
}
pub(super) fn yog_methods(&mut self, group: &str, name: &str, at: Span) -> Callee {
let surface = &*super::surface::SURFACE;
let Some(g) = surface.groups.get(group) else {
return Callee::Unknown;
};
let Some(methods) = g.methods.get(name) else {
self.unknown_member(&Type::Yog(group.to_string()), name, at);
return Callee::Unknown;
};
if !self.to_server
&& methods
.iter()
.any(|m| m.address == yog_ids::service_net::SEND_TO_SERVER)
{
self.report.error(
script_problem::UNDECLARED,
at,
"this mod sends to the server without saying so: `[network]` `to_server = true` in yog.toml (it then needs Yog on the server too)",
);
}
Callee::Typed(
methods
.iter()
.map(|m| Signature {
params: m
.params
.iter()
.map(|p| Type::of_shape(&(p.shape)()))
.collect(),
passes: vec![None; m.params.len()],
required: m.params.iter().take_while(|p| p.required).count(),
ret: Type::of_shape(&m.reply),
generics: Vec::new(),
bound: Default::default(),
rest: false,
})
.collect(),
)
}
pub(super) fn game_methods(&mut self, class: &str, name: &str, at: Span) -> Callee {
let Some(lookup) = &self.lookup else {
return Callee::Unknown;
};
let Some(c) = lookup.class(class) else {
return Callee::Unknown;
};
match game::member(lookup, c, name, true) {
Found::Methods(sigs) => Callee::Typed(
sigs.into_iter()
.map(|(params, ret)| Signature {
passes: vec![None; params.len()],
required: params.len(),
params,
ret,
generics: Vec::new(),
bound: Default::default(),
rest: false,
})
.collect(),
),
Found::Nothing => {
self.unknown_member(&Type::Game(class.to_string()), name, at);
Callee::Unknown
}
_ => Callee::Unknown,
}
}
}
pub(super) fn is_side_call(name: &str) -> bool {
[&crate::words::client::WORD, &crate::words::server::WORD]
.iter()
.any(|w| yog_codegen::pascal(w.text) == name)
}
fn signature_of(params: &[Type], ret: &Type) -> Signature {
Signature {
params: params.to_vec(),
passes: vec![None; params.len()],
required: params.len(),
ret: ret.clone(),
generics: Vec::new(),
bound: Default::default(),
rest: false,
}
}
pub(super) fn own_args(t: &Type) -> &[Type] {
match t {
Type::Own(_, args) | Type::Static(_, args) => args,
_ => &[],
}
}
impl Checker<'_, '_, '_> {
fn unbound(&self, methods: &[Method]) -> Vec<(Method, Context)> {
let here = self.here();
methods.iter().map(|m| (m.clone(), here.clone())).collect()
}
}
impl Checker<'_, '_, '_> {
pub(super) fn hosted(&self, t: &Type) -> Option<Type> {
if let Type::Content(_) = t {
let of = yog_script_ids::system::CONTENT_ID.to_string();
return Some(Type::Own(of, vec![t.clone()]));
}
if let Type::Tag(kind) = t {
let of = yog_script_ids::system::TAG_ID.to_string();
return Some(Type::Own(of, vec![Type::Content(kind.clone())]));
}
let name = match t {
Type::Array(..) => yog_script_ids::system::ARRAY,
_ => super::builtin::system_name(t)?,
};
[name, yog_script_ids::clr::OBJECT]
.into_iter()
.find(|n| {
self.own
.namespaces
.get(*n)
.is_some_and(|ns| *ns == [yog_script_ids::system::ROOT])
})
.map(|n| Type::own(n.to_string()))
}
}
impl Checker<'_, '_, '_> {
pub(super) fn extension_or_unknown(&mut self, on: &Type, name: &crate::ast::Name) -> Callee {
let methods: Vec<(Method, Context)> = self
.own
.extension_methods
.get(&name.text)
.into_iter()
.flatten()
.filter(|(key, _)| self.own_in_reach(&key.name))
.map(|(key, m)| (m.clone(), self.context_of(&key.name, &[])))
.collect();
if !methods.is_empty() {
return Callee::Extension(methods);
}
if on.known() {
self.unknown_member(on, &name.text, name.span);
}
Callee::Unknown
}
}
impl Checker<'_, '_, '_> {
pub(super) fn statics_type(&self, of: &str, args: &[Type]) -> Type {
if !args.is_empty() {
return Type::Own(of.to_string(), args.to_vec());
}
let params = self
.own
.named(of)
.map(|(_, t)| t.generics.iter().map(|g| Type::Param(g.clone())).collect())
.unwrap_or_default();
Type::Own(of.to_string(), params)
}
}