use super::*;
use rustc_hash::FxHashMap;
use std::cell::RefCell;
pub(crate) fn parse_assertion_type(s: &str) -> (Type, bool) {
match s.trim().strip_prefix('!') {
Some(rest) => (parse_type_string(rest), true),
None => (parse_type_string(s), false),
}
}
pub(crate) fn split_array_key_suffix(name: &str) -> (String, Vec<mir_types::ArrayKey>) {
let mut rest = name;
let mut keys = Vec::new();
while rest.ends_with(']') {
let Some(open) = rest.rfind('[') else {
return (name.to_string(), Vec::new());
};
if open == 0 {
return (name.to_string(), Vec::new());
}
let inner = &rest[open + 1..rest.len() - 1];
let Some(key) = parse_literal_array_key(inner.trim()) else {
return (name.to_string(), Vec::new());
};
keys.push(key);
rest = &rest[..open];
}
if rest.is_empty() {
return (name.to_string(), Vec::new());
}
keys.reverse();
(rest.to_string(), keys)
}
fn parse_literal_array_key(inner: &str) -> Option<mir_types::ArrayKey> {
if let Some(s) = inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
return Some(mir_types::ArrayKey::String(std::sync::Arc::from(s)));
}
if let Some(s) = inner.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
return Some(mir_types::ArrayKey::String(std::sync::Arc::from(s)));
}
inner.parse::<i64>().ok().map(mir_types::ArrayKey::Int)
}
pub(crate) fn parse_type_string(s: &str) -> Type {
let s = s.trim();
if let Some(inner) = s.strip_prefix('?') {
let inner_ty = parse_type_string(inner);
let mut u = inner_ty;
u.add_type(Atomic::TNull);
return u;
}
if s.starts_with('(') && s.ends_with(')') {
let inner = s[1..s.len() - 1].trim();
if let Some(conditional) = parse_conditional_type(inner) {
return conditional;
}
if is_balanced_parens(s) {
return parse_type_string(inner);
}
}
if s.contains('|') && !is_inside_generics(s) {
let parts = split_union(s);
if parts.len() > 1 {
let mut u = Type::empty();
for part in parts {
for atomic in parse_type_string(&part).types {
u.add_type(atomic);
}
}
return u;
}
}
if s.contains('&') && !is_inside_generics(s) {
let parts = split_intersection(s);
if parts.len() > 1 {
let parts: Vec<Type> = parts.iter().map(|p| parse_type_string(p.trim())).collect();
return Type::single(Atomic::TIntersection {
parts: mir_types::union::vec_to_type_params(parts),
});
}
}
if let Some(value_str) = s.strip_suffix("[]") {
let value = parse_type_string(value_str);
return Type::single(Atomic::TArray {
key: Box::new(Type::array_key()),
value: Box::new(value),
});
}
if let Some(call_ty) = parse_callable_syntax(s) {
return call_ty;
}
if s.ends_with('}') {
if let Some(open) = s.find('{') {
let prefix = s[..open].to_lowercase();
let inner = &s[open + 1..s.len() - 1];
if prefix == "array" {
return parse_keyed_array(inner, false);
} else if prefix == "list" {
return parse_keyed_array(inner, true);
} else if prefix == "object" {
return Type::single(Atomic::TObject);
}
}
}
if let Some(open) = s.find('<') {
if s.ends_with('>') {
let name = &s[..open];
let inner = &s[open + 1..s.len() - 1];
return parse_generic(name, inner);
}
}
if let Some(f) = parse_float_literal(s) {
let bits = f.to_bits();
return Type::single(Atomic::TLiteralFloat(
(bits >> 32) as i64,
(bits & 0xFFFF_FFFF) as i64,
));
}
match s.to_lowercase().as_str() {
"string" => Type::single(Atomic::TString),
"non-empty-string" => Type::single(Atomic::TNonEmptyString),
"numeric-string" => Type::single(Atomic::TNumericString),
"truthy-string" | "non-falsy-string" => Type::single(Atomic::TNonEmptyString),
"lowercase-string"
| "uppercase-string"
| "non-empty-lowercase-string"
| "non-empty-uppercase-string" => Type::single(Atomic::TString),
"class-string" => Type::single(Atomic::TClassString(None)),
"interface-string" => Type::single(Atomic::TInterfaceString(None)),
"int" | "integer" => Type::single(Atomic::TInt),
"literal-int" => Type::single(Atomic::TInt),
"literal-string" => Type::single(Atomic::TString),
"positive-int" => Type::single(Atomic::TPositiveInt),
"negative-int" => Type::single(Atomic::TNegativeInt),
"non-negative-int" => Type::single(Atomic::TNonNegativeInt),
"float" | "double" => Type::single(Atomic::TFloat),
"bool" | "boolean" => Type::single(Atomic::TBool),
"true" => Type::single(Atomic::TTrue),
"false" => Type::single(Atomic::TFalse),
"null" => Type::single(Atomic::TNull),
"void" => Type::single(Atomic::TVoid),
"never" | "never-return" | "no-return" | "never-returns" => Type::single(Atomic::TNever),
"mixed" => Type::single(Atomic::TMixed),
"object" => Type::single(Atomic::TObject),
"array" => Type::single(Atomic::TArray {
key: Box::new(Type::array_key()),
value: Box::new(Type::mixed()),
}),
"list" => Type::single(Atomic::TList {
value: Box::new(Type::mixed()),
}),
"callable" | "pure-callable" => Type::single(Atomic::TCallable {
params: None,
return_type: None,
}),
"pure-closure" => Type::single(Atomic::TNamedObject {
fqcn: mir_types::Name::from("Closure"),
type_params: Default::default(),
}),
"callable-string" => Type::single(Atomic::TCallableString),
"iterable" => {
let mut u = Type::single(Atomic::TArray {
key: Box::new(Type::array_key()),
value: Box::new(Type::mixed()),
});
u.add_type(Atomic::TNamedObject {
fqcn: mir_types::Name::from("Traversable"),
type_params: Default::default(),
});
u
}
"scalar" => Type::single(Atomic::TScalar),
"numeric" => Type::single(Atomic::TNumeric),
"empty" => {
let mut u = Type::single(Atomic::TFalse);
u.add_type(Atomic::TNull);
u.add_type(Atomic::TLiteralInt(0));
u.add_type(Atomic::TLiteralFloat(0, 0));
u.add_type(Atomic::TLiteralString(Arc::from("")));
u.add_type(Atomic::TLiteralString(Arc::from("0")));
u.add_type(Atomic::TKeyedArray {
properties: Box::default(),
is_open: false,
is_list: true,
});
u
}
"array-key" => {
let mut u = Type::single(Atomic::TInt);
u.add_type(Atomic::TString);
u
}
"resource" => Type::mixed(), "static" => Type::single(Atomic::TStaticObject {
fqcn: mir_types::Name::from(""),
}),
"self" | "$this" => Type::single(Atomic::TSelf {
fqcn: mir_types::Name::from(""),
}),
"parent" => Type::single(Atomic::TParent {
fqcn: mir_types::Name::from(""),
}),
_ if !s.is_empty()
&& s.chars()
.next()
.map(|c| c.is_alphanumeric() || c == '\\' || c == '_')
.unwrap_or(false) =>
{
if let Ok(n) = s.parse::<i64>() {
return Type::single(Atomic::TLiteralInt(n));
}
Type::single(Atomic::TNamedObject {
fqcn: normalize_fqcn(s).into(),
type_params: mir_types::union::empty_type_params(),
})
}
_ if s.starts_with('-') && s.len() > 1 && s[1..].chars().all(|c| c.is_ascii_digit()) => {
if let Ok(n) = s.parse::<i64>() {
Type::single(Atomic::TLiteralInt(n))
} else {
Type::mixed()
}
}
_ if s.len() >= 2
&& ((s.starts_with('\'') && s.ends_with('\''))
|| (s.starts_with('"') && s.ends_with('"'))) =>
{
let inner = &s[1..s.len() - 1];
Type::single(Atomic::TLiteralString(Arc::from(inner)))
}
_ => Type::mixed(),
}
}
pub(super) fn parse_generic(name: &str, inner: &str) -> Type {
match name.to_lowercase().as_str() {
"array" => {
let params = split_generics(inner);
let (key, value) = match params.len() {
n if n >= 2 => (
parse_type_string(params[0].trim()),
parse_type_string(params[1].trim()),
),
1 => (Type::array_key(), parse_type_string(params[0].trim())),
_ => (Type::array_key(), Type::mixed()),
};
Type::single(Atomic::TArray {
key: Box::new(key),
value: Box::new(value),
})
}
"list" | "non-empty-list" => {
let value = parse_type_string(inner.trim());
if name.to_lowercase().starts_with("non-empty") {
Type::single(Atomic::TNonEmptyList {
value: Box::new(value),
})
} else {
Type::single(Atomic::TList {
value: Box::new(value),
})
}
}
"non-empty-array" => {
let params = split_generics(inner);
let (key, value) = match params.len() {
n if n >= 2 => (
parse_type_string(params[0].trim()),
parse_type_string(params[1].trim()),
),
1 => (Type::array_key(), parse_type_string(params[0].trim())),
_ => (Type::array_key(), Type::mixed()),
};
Type::single(Atomic::TNonEmptyArray {
key: Box::new(key),
value: Box::new(value),
})
}
"iterable" => {
let params = split_generics(inner);
let (key, value) = match params.len() {
n if n >= 2 => (
parse_type_string(params[0].trim()),
parse_type_string(params[1].trim()),
),
1 => (Type::array_key(), parse_type_string(params[0].trim())),
_ => (Type::array_key(), Type::mixed()),
};
let mut u = Type::single(Atomic::TArray {
key: Box::new(key.clone()),
value: Box::new(value.clone()),
});
u.add_type(Atomic::TNamedObject {
fqcn: mir_types::Name::from("Traversable"),
type_params: mir_types::union::vec_to_type_params(vec![key, value]),
});
u
}
"class-string" => Type::single(Atomic::TClassString(Some(
normalize_fqcn(inner.trim()).into(),
))),
"interface-string" => Type::single(Atomic::TInterfaceString(Some(
normalize_fqcn(inner.trim()).into(),
))),
"int" => {
let parse_bound = |s: &str| -> Option<i64> {
match s.trim() {
"min" | "max" => None,
n => n.parse::<i64>().ok(),
}
};
let bounds = split_generics(inner);
let (min, max) = match bounds.as_slice() {
[lo, hi] => (parse_bound(lo), parse_bound(hi)),
_ => (None, None),
};
Type::single(Atomic::TIntRange { min, max })
}
"key-of" => {
let inner_ty = parse_type_string(inner.trim());
eval_key_of(&inner_ty).unwrap_or_else(Type::mixed)
}
"value-of" => {
let inner_ty = parse_type_string(inner.trim());
eval_value_of(&inner_ty).unwrap_or_else(Type::mixed)
}
"int-mask" => {
let parts = split_generics(inner);
let members: Vec<i64> = parts
.iter()
.filter_map(|s| s.trim().parse::<i64>().ok())
.collect();
if !members.is_empty() && members.len() == parts.len() {
expand_int_mask_members(&members).unwrap_or_else(|| Type::single(Atomic::TInt))
} else {
Type::single(Atomic::TInt)
}
}
"int-mask-of" => resolve_int_mask_of(inner).unwrap_or_else(|| Type::single(Atomic::TInt)),
"class-string-map" => {
let params = split_generics(inner);
let value = params
.get(1)
.or(params.first())
.map(|p| parse_type_string(p.trim()))
.unwrap_or_else(Type::mixed);
Type::single(Atomic::TArray {
key: Box::new(Type::single(Atomic::TClassString(None))),
value: Box::new(value),
})
}
"static" => Type::single(Atomic::TStaticObject {
fqcn: mir_types::Name::from(""),
}),
"self" | "$this" => Type::single(Atomic::TSelf {
fqcn: mir_types::Name::from(""),
}),
"parent" => Type::single(Atomic::TParent {
fqcn: mir_types::Name::from(""),
}),
_ => {
let params: Vec<Type> = split_generics(inner)
.iter()
.map(|p| parse_type_string(p.trim()))
.collect();
Type::single(Atomic::TNamedObject {
fqcn: normalize_fqcn(name).into(),
type_params: mir_types::union::vec_to_type_params(params),
})
}
}
}
pub(super) fn parse_float_literal(s: &str) -> Option<f64> {
if !s.contains('.') {
return None;
}
let body = s.strip_prefix('-').unwrap_or(s);
if !body
.chars()
.all(|c| c.is_ascii_digit() || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-')
{
return None;
}
s.parse::<f64>().ok()
}
pub(super) fn eval_key_of(t: &Type) -> Option<Type> {
let mut result = Type::empty();
for atomic in &t.types {
match atomic {
Atomic::TArray { key, .. } | Atomic::TNonEmptyArray { key, .. } => {
for k in &key.types {
result.add_type(k.clone());
}
}
Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => {
result.add_type(Atomic::TInt);
}
Atomic::TKeyedArray { properties, .. } => {
for key in properties.keys() {
match key {
mir_types::atomic::ArrayKey::Int(n) => {
result.add_type(Atomic::TLiteralInt(*n));
}
mir_types::atomic::ArrayKey::String(s) => {
result.add_type(Atomic::TLiteralString(s.clone()));
}
}
}
}
_ => return None,
}
}
(!result.types.is_empty()).then_some(result)
}
pub(super) fn eval_value_of(t: &Type) -> Option<Type> {
let mut result = Type::empty();
for atomic in &t.types {
match atomic {
Atomic::TArray { value, .. }
| Atomic::TNonEmptyArray { value, .. }
| Atomic::TList { value }
| Atomic::TNonEmptyList { value } => {
for v in &value.types {
result.add_type(v.clone());
}
}
Atomic::TKeyedArray { properties, .. } => {
for prop in properties.values() {
for v in &prop.ty.types {
result.add_type(v.clone());
}
}
}
_ => return None,
}
}
(!result.types.is_empty()).then_some(result)
}
pub(super) fn expand_int_mask_members(members: &[i64]) -> Option<Type> {
if members.is_empty() || members.len() > 8 || members.iter().any(|&v| v < 0) {
return None;
}
let mut values = std::collections::BTreeSet::new();
for subset in 0u32..(1u32 << members.len()) {
let value = members
.iter()
.enumerate()
.filter(|(i, _)| subset & (1 << i) != 0)
.fold(0i64, |acc, (_, &v)| acc | v);
values.insert(value);
}
let mut u = Type::empty();
for v in values {
u.add_type(Atomic::TLiteralInt(v));
}
Some(u)
}
type SelfIntConstants = (Arc<str>, Arc<FxHashMap<Arc<str>, i64>>);
thread_local! {
static SELF_INT_CONSTANTS: RefCell<Option<SelfIntConstants>> = const {
RefCell::new(None)
};
}
pub(crate) struct SelfIntConstantsGuard {
previous: Option<SelfIntConstants>,
}
impl SelfIntConstantsGuard {
pub(crate) fn activate(fqcn: &str, constants: &Arc<FxHashMap<Arc<str>, i64>>) -> Self {
let previous = SELF_INT_CONSTANTS
.with(|cell| cell.replace(Some((Arc::from(fqcn), constants.clone()))));
Self { previous }
}
}
impl Drop for SelfIntConstantsGuard {
fn drop(&mut self) {
SELF_INT_CONSTANTS.with(|cell| *cell.borrow_mut() = self.previous.take());
}
}
pub(super) fn resolve_int_mask_of(inner: &str) -> Option<Type> {
let (class_ref, pattern) = inner.trim().split_once("::")?;
let class_ref = class_ref.trim();
let prefix = pattern.trim().strip_suffix('*')?;
SELF_INT_CONSTANTS.with(|cell| {
let active = cell.borrow();
let (fqcn, constants) = active.as_ref()?;
let short_name = fqcn.rsplit('\\').next().unwrap_or(fqcn);
let is_self_ref = matches!(class_ref, "self" | "static" | "$this")
|| normalize_fqcn(class_ref).eq_ignore_ascii_case(fqcn)
|| (!class_ref.contains('\\') && class_ref.eq_ignore_ascii_case(short_name));
if !is_self_ref {
return None;
}
let mut members: Vec<i64> = constants
.iter()
.filter(|(name, _)| name.starts_with(prefix))
.map(|(_, &v)| v)
.collect();
members.sort_unstable();
members.dedup();
expand_int_mask_members(&members)
})
}
pub(super) fn strip_ascii_ci_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let bytes = s.as_bytes();
(bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
.then(|| &s[prefix.len()..])
}
pub(super) fn strip_quotes(s: &str) -> &str {
if s.len() >= 2
&& ((s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')))
{
&s[1..s.len() - 1]
} else {
s
}
}
pub(super) fn parse_keyed_array(inner: &str, is_list: bool) -> Type {
use mir_types::atomic::KeyedProperty;
let mut properties: IndexMap<ArrayKey, KeyedProperty> = IndexMap::new();
let mut is_open = false;
let mut auto_index = 0i64;
for item in split_generics(inner) {
let item = item.trim();
if item.is_empty() {
continue;
}
if item == "..." || item.starts_with("...") {
is_open = true;
continue;
}
let colon_pos = {
let mut depth = 0i32;
let mut found = None;
for (i, ch) in item.char_indices() {
match ch {
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth -= 1,
':' if depth == 0 => {
found = Some(i);
break;
}
_ => {}
}
}
found
};
if let Some(colon) = colon_pos {
let key_part = item[..colon].trim();
let ty_part = item[colon + 1..].trim();
let optional = key_part.ends_with('?');
let key_str = key_part.trim_end_matches('?').trim();
let key_str = strip_quotes(key_str);
let key = if let Ok(n) = key_str.parse::<i64>() {
ArrayKey::Int(n)
} else {
ArrayKey::String(Arc::from(key_str))
};
properties.insert(
key,
KeyedProperty {
ty: parse_type_string(ty_part),
optional,
},
);
} else {
properties.insert(
ArrayKey::Int(auto_index),
KeyedProperty {
ty: parse_type_string(item),
optional: false,
},
);
auto_index += 1;
}
}
Type::single(Atomic::TKeyedArray {
properties: Box::new(properties),
is_open,
is_list,
})
}
pub(super) fn parse_callable_syntax(s: &str) -> Option<Type> {
let s = s.trim_start_matches('\\');
let s_after_pure = strip_ascii_ci_prefix(s, "pure-").unwrap_or(s);
let (is_closure, rest) = if let Some(rest) = strip_ascii_ci_prefix(s_after_pure, "closure") {
(true, rest)
} else {
let rest = strip_ascii_ci_prefix(s_after_pure, "callable")?;
(false, rest)
};
let rest = rest.trim_start();
if !rest.starts_with('(') {
return None;
}
let close = find_matching_paren(rest)?;
let params_str = &rest[1..close];
let after = rest[close + 1..].trim();
let return_type = after
.strip_prefix(':')
.map(|ret_str| Box::new(parse_type_string(ret_str.trim())));
let params: Box<[mir_types::atomic::FnParam]> = split_generics(params_str)
.into_iter()
.enumerate()
.filter(|(_, p)| !p.trim().is_empty())
.map(|(i, p)| {
let p = p.trim();
let (p, is_variadic) = match p.strip_prefix("...") {
Some(rest) => (rest.trim_start(), true),
None => (p, false),
};
let (p, is_optional) = match p.strip_suffix('=') {
Some(rest) => (rest.trim_end(), true),
None => (p, false),
};
let (ty_str, name) = if let Some(dollar) = p.rfind('$') {
(p[..dollar].trim(), p[dollar + 1..].to_string())
} else {
(p, format!("arg{i}"))
};
mir_types::atomic::FnParam {
name: name.into(),
ty: Some(mir_types::SimpleType::from_union(parse_type_string(ty_str))),
out_ty: None,
default: None,
is_variadic,
is_byref: false,
is_optional: is_optional || is_variadic,
}
})
.collect();
if is_closure {
Some(Type::single(Atomic::TClosure {
data: Box::new(mir_types::atomic::ClosureData {
params,
return_type: return_type
.map_or_else(|| Type::single(Atomic::TVoid), |boxed| *boxed),
this_type: None,
}),
}))
} else {
Some(Type::single(Atomic::TCallable {
params: Some(params),
return_type,
}))
}
}
pub(super) fn find_matching_paren(s: &str) -> Option<usize> {
if !s.starts_with('(') {
return None;
}
let mut depth = 0i32;
for (i, ch) in s.char_indices() {
match ch {
'(' | '<' | '{' => depth += 1,
')' | '>' | '}' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
pub(super) fn parse_template_line(
_tag_name: &str,
body: Option<String>,
) -> Option<(String, Option<String>, Option<String>)> {
let body = body?;
let body = body.trim();
let body = body.lines().next().unwrap_or(body).trim_end();
let body = match body.find(" @") {
Some(idx) => body[..idx].trim_end(),
None => body,
};
if body.is_empty() {
return None;
}
let mut tokens = body.split_whitespace().peekable();
let name = tokens.next()?;
let bound = if matches!(tokens.peek(), Some(&"of") | Some(&"as")) {
tokens.next();
let mut bound_tokens: Vec<&str> = Vec::new();
let mut depth: i32 = 0;
while let Some(&t) = tokens.peek() {
if t == "=" && depth == 0 {
break;
}
if depth == 0 && !bound_tokens.is_empty() {
let prev_ends_with_op = bound_tokens
.last()
.is_some_and(|p| p.ends_with('|') || p.ends_with('&'));
let starts_with_op = t.starts_with('|') || t.starts_with('&');
if !prev_ends_with_op && !starts_with_op {
break;
}
}
for c in t.chars() {
match c {
'<' | '(' | '[' | '{' => depth += 1,
'>' | ')' | ']' | '}' => depth -= 1,
_ => {}
}
}
bound_tokens.push(t);
tokens.next();
}
let bound = bound_tokens.join(" ");
(!bound.is_empty()).then_some(bound)
} else {
None
};
let default = if matches!(tokens.peek(), Some(&"=")) {
tokens.next();
let default: String = tokens.collect::<Vec<_>>().join(" ");
(!default.is_empty()).then_some(default)
} else {
None
};
Some((name.to_string(), bound, default))
}
pub(super) fn extract_description(text: &str) -> String {
let mut desc_lines: Vec<&str> = Vec::new();
for line in text.lines() {
let l = line.trim();
let l = l.trim_start_matches("/**").trim();
let l = l.trim_end_matches("*/").trim();
let l = l.trim_start_matches("*/").trim();
let l = l.strip_prefix("* ").unwrap_or(l.trim_start_matches('*'));
let l = l.trim();
if l.starts_with('@') {
break;
}
if !l.is_empty() {
desc_lines.push(l);
}
}
desc_lines.join(" ")
}
pub(super) fn parse_import_type(body: &str) -> Option<DocImportType> {
let (before_from, from_class_raw) = body.split_once(" from ")?;
let from_class = from_class_raw.trim().trim_start_matches('\\').to_string();
if from_class.is_empty() {
return None;
}
let (original, local) = if let Some((orig, loc)) = before_from.split_once(" as ") {
(orig.trim().to_string(), loc.trim().to_string())
} else {
let name = before_from.trim().to_string();
(name.clone(), name)
};
if original.is_empty() || local.is_empty() {
return None;
}
Some(DocImportType {
original,
local,
from_class,
})
}
pub(super) fn parse_param_line(s: &str) -> Option<(String, String)> {
let mut depth: i32 = 0;
for (i, ch) in s.char_indices() {
match ch {
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth = (depth - 1).max(0),
_ if ch.is_whitespace() && depth == 0 => {
let after = s[i..].trim_start();
let after_stripped = after.strip_prefix('&').unwrap_or(after);
let after_stripped = after_stripped.strip_prefix("...").unwrap_or(after_stripped);
if after_stripped.starts_with('$') {
if let Some(name_with_dollar) = after_stripped.split(char::is_whitespace).next()
{
let name = name_with_dollar.trim_start_matches('$').to_string();
if !name.is_empty() {
let type_part = s[..i].trim().to_string();
if !type_part.is_empty() {
return Some((type_part, name));
}
}
}
}
}
_ => {}
}
}
None
}
pub(super) fn parse_var_line(s: &str) -> Option<(String, String)> {
let mut depth: i32 = 0;
for (i, ch) in s.char_indices() {
match ch {
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth = (depth - 1).max(0),
_ if ch.is_whitespace() && depth == 0 => {
let after = s[i..].trim_start();
let after_stripped = after.strip_prefix('&').unwrap_or(after);
let after_stripped = after_stripped.strip_prefix("...").unwrap_or(after_stripped);
if after_stripped.starts_with('$') {
if let Some(name_with_dollar) = after_stripped.split(char::is_whitespace).next()
{
let name = name_with_dollar.trim_start_matches('$').to_string();
if !name.is_empty() {
let type_part = s[..i].trim().to_string();
if !type_part.is_empty() {
return Some((type_part, name));
}
}
}
}
return None;
}
_ => {}
}
}
None
}
pub(super) fn extract_return_type(s: &str) -> String {
let mut depth: i32 = 0;
let mut current_token = String::new();
for ch in s.chars() {
match ch {
'<' | '(' | '{' => {
depth += 1;
current_token.push(ch);
}
'>' | ')' | '}' => {
depth = (depth - 1).max(0);
current_token.push(ch);
}
_ if ch.is_whitespace() && depth == 0 => {
break;
}
_ => {
current_token.push(ch);
}
}
}
if current_token.ends_with(':') {
let offset = current_token.len();
let rest = s[offset..].trim_start();
if !rest.is_empty() {
let ret_type = extract_return_type(rest);
current_token.push_str(&ret_type);
}
}
current_token.trim().to_string()
}
pub(super) fn split_union(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0;
let mut in_quote: Option<char> = None;
let mut current = String::new();
for ch in s.chars() {
if let Some(q) = in_quote {
current.push(ch);
if ch == q {
in_quote = None;
}
continue;
}
match ch {
'\'' | '"' => {
in_quote = Some(ch);
current.push(ch);
}
'<' | '(' | '{' => {
depth += 1;
current.push(ch);
}
'>' | ')' | '}' => {
depth -= 1;
current.push(ch);
}
'|' if depth == 0 => {
parts.push(current.trim().to_string());
current = String::new();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
}
pub(super) fn split_intersection(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0i32;
let mut current = String::new();
for ch in s.chars() {
match ch {
'<' | '(' | '{' => {
depth += 1;
current.push(ch);
}
'>' | ')' | '}' => {
depth -= 1;
current.push(ch);
}
'&' if depth == 0 => {
parts.push(current.trim().to_string());
current = String::new();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
}
pub(super) fn is_balanced_parens(s: &str) -> bool {
if !s.starts_with('(') || !s.ends_with(')') {
return false;
}
let mut depth = 0i32;
let chars: Vec<char> = s.chars().collect();
let last = chars.len() - 1;
for (i, ch) in chars.iter().enumerate() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 && i < last {
return false;
}
}
_ => {}
}
}
depth == 0
}
pub(super) fn split_generics(s: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0;
let mut current = String::new();
for ch in s.chars() {
match ch {
'<' | '(' | '{' => {
depth += 1;
current.push(ch);
}
'>' | ')' | '}' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
parts.push(current.trim().to_string());
current = String::new();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
parts.push(current.trim().to_string());
}
parts
}
pub(super) fn extract_type_prefix(s: &str) -> &str {
let mut depth = 0i32;
let mut end = s.len();
for (i, ch) in s.char_indices() {
match ch {
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth -= 1,
_ if ch.is_whitespace() && depth == 0 => {
end = i;
break;
}
_ => {}
}
}
&s[..end]
}
pub(super) fn contains_unquoted(s: &str, target: char) -> bool {
let mut in_quote: Option<char> = None;
for ch in s.chars() {
if let Some(q) = in_quote {
if ch == q {
in_quote = None;
}
continue;
}
match ch {
'\'' | '"' => in_quote = Some(ch),
_ if ch == target => return true,
_ => {}
}
}
false
}
pub(super) fn has_unterminated_quote(s: &str) -> bool {
let mut in_quote: Option<char> = None;
for ch in s.chars() {
match in_quote {
Some(q) if ch == q => in_quote = None,
Some(_) => {}
None if ch == '\'' || ch == '"' => in_quote = Some(ch),
None => {}
}
}
in_quote.is_some()
}
pub(super) fn is_inside_generics(s: &str) -> bool {
let mut depth = 0i32;
let mut in_quote: Option<char> = None;
for ch in s.chars() {
if let Some(q) = in_quote {
if ch == q {
in_quote = None;
}
continue;
}
match ch {
'\'' | '"' => in_quote = Some(ch),
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth -= 1,
_ => {}
}
}
depth != 0
}
pub(super) fn parse_conditional_type(s: &str) -> Option<Type> {
let (is_pos, is_marker_len, negated) = find_is_marker_at_depth(s)?;
let param_raw = s[..is_pos].trim();
let param_name_str: &str = if let Some(name) = param_raw.strip_prefix('$') {
if name.is_empty() {
return None;
}
name
} else {
if param_raw.is_empty()
|| !param_raw.starts_with(|c: char| c.is_alphabetic() || c == '_')
|| !param_raw.chars().all(|c| c.is_alphanumeric() || c == '_')
|| !s.contains('?')
{
return None;
}
param_raw
};
let param_name = Some(mir_types::Name::new(param_name_str));
let after_is = s[is_pos + is_marker_len..].trim();
let q_pos = find_char_at_depth(after_is, '?')?;
let subject_str = after_is[..q_pos].trim();
let rest = after_is[q_pos + 1..].trim();
let colon_pos = find_char_at_depth(rest, ':')?;
let true_str = rest[..colon_pos].trim();
let false_str = rest[colon_pos + 1..].trim();
let (if_true_str, if_false_str) = if negated {
(false_str, true_str)
} else {
(true_str, false_str)
};
Some(Type::single(Atomic::TConditional {
data: Box::new(mir_types::atomic::ConditionalData {
param_name,
subject: parse_type_string(subject_str),
if_true: parse_type_string(if_true_str),
if_false: parse_type_string(if_false_str),
}),
}))
}
fn find_is_marker_at_depth(s: &str) -> Option<(usize, usize, bool)> {
let mut depth = 0i32;
let bytes = s.as_bytes();
for (i, ch) in s.char_indices() {
match ch {
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth -= 1,
' ' if depth == 0 => {
if bytes[i..].starts_with(b" is not ") {
return Some((i, " is not ".len(), true));
}
if bytes[i..].starts_with(b" is ") {
return Some((i, " is ".len(), false));
}
}
_ => {}
}
}
None
}
pub(super) fn find_char_at_depth(s: &str, target: char) -> Option<usize> {
let mut depth = 0i32;
for (i, ch) in s.char_indices() {
match ch {
'<' | '(' | '{' => depth += 1,
'>' | ')' | '}' => depth -= 1,
_ if ch == target && depth == 0 => return Some(i),
_ => {}
}
}
None
}