1use palette::Srgb;
2use rbx_types::{Color3uint8, Content, EnumItem, UDim, Variant};
3use rbx_types_ops::BasicOperations;
4
5use crate::lexer::Token;
6use crate::parser::types::{Construct, Delimited, Node};
7
8use crate::datatype::colors::{BRICK_COLORS, CSS_COLORS, SKIN_COLORS, TAILWIND_COLORS};
9use crate::datatype::lookup::StaticLookup;
10use crate::datatype::tuple;
11use crate::datatype::types::Datatype;
12use crate::datatype::variants::EnumItemFromNameAndValueName;
13
14pub fn evaluate_construct(
15 construct: &Construct,
16 key: Option<&str>,
17 lookup: &dyn StaticLookup,
18) -> Option<Datatype> {
19 match construct {
20 Construct::Node { node } => evaluate_token(node, key, lookup),
21
22 Construct::MathOperation {
23 left,
24 operators,
25 right,
26 } => {
27 let left_val = evaluate_construct(left, key, lookup)?;
28 let right_val = right
29 .as_ref()
30 .and_then(|r| evaluate_construct(r, key, lookup));
31
32 let Some(right_val) = right_val else {
33 return Some(left_val);
34 };
35
36 let left_variant = left_val.coerce_to_variant(key)?;
37 let right_variant = right_val.coerce_to_variant(key)?;
38
39 let result = if let Some(first_op) = operators.first() {
40 apply_operator(first_op, &left_variant, &right_variant)
41 } else {
42 None
43 };
44
45 result.map(Datatype::Variant)
46 }
47
48 Construct::UnaryMinus { operand, .. } => {
49 let val = evaluate_construct(operand, key, lookup)?;
50 let variant = val.coerce_to_variant(key)?;
51 negate_variant(&variant).map(Datatype::Variant)
52 }
53
54 Construct::Table { body } => {
55 let datatypes = evaluate_delimited_to_vec(body, lookup);
56 coerce_tuple_data(datatypes, None)
57 }
58
59 Construct::AnnotatedTable { annotation, body } => {
60 let annotation_name = match annotation.token.value() {
61 Token::Identifier(name) => Some(*name),
62 _ => None,
63 };
64
65 if let Some(body) = body {
66 let datatypes = evaluate_delimited_to_vec(body, lookup);
67 coerce_tuple_data(datatypes, annotation_name)
68 } else {
69 coerce_tuple_data(vec![], annotation_name)
70 }
71 }
72
73 Construct::Enum { name, variant, .. } => {
74 let enum_name = name.as_ref().and_then(|n| match n.token.value() {
75 Token::TagSelectorOrEnumPart(Some(s)) => Some(*s),
76 _ => None,
77 });
78
79 let enum_value = variant.as_ref().and_then(|v| match v.token.value() {
80 Token::Identifier(s) => Some(*s),
81 Token::TagSelectorOrEnumPart(Some(s)) => Some(*s),
82 Token::StateSelectorOrEnumPart(Some(s)) => Some(*s),
83 _ => None,
84 });
85
86 match (enum_name, enum_value) {
87 (Some(name), Some(value)) => EnumItem::from_name_and_value_name(name, value)
88 .map(|item| Datatype::Variant(Variant::EnumItem(item)))
89 .or(Some(Datatype::None)),
90 _ => Some(Datatype::None),
91 }
92 }
93
94 Construct::Assignment { right, .. } => right
95 .as_ref()
96 .and_then(|r| evaluate_construct(r, key, lookup)),
97
98 _ => None,
99 }
100}
101
102fn evaluate_token(
103 node: &Node,
104 key: Option<&str>,
105 lookup: &dyn StaticLookup,
106) -> Option<Datatype> {
107 match node.token.value() {
108 Token::Number(s) => parse_number_str(s).map(|n| Datatype::Variant(Variant::Float64(n))),
109
110 Token::NumberOffset(s) => {
111 let num_str = s.strip_suffix("px").unwrap_or(s);
112 let offset = parse_number_str(num_str).map(|n| n as i32).unwrap_or(0);
113 Some(Datatype::Variant(Variant::UDim(UDim::new(0.0, offset))))
114 }
115
116 Token::NumberScale(s) => {
117 let num_str = s.strip_suffix('%').unwrap_or(s);
118 let scale = parse_number_str(num_str).unwrap_or(0.0) / 100.0;
119 Some(Datatype::Variant(Variant::UDim(UDim::new(scale as f32, 0))))
120 }
121
122 Token::StringSingle(s) => Some(Datatype::Variant(Variant::String(s.to_string()))),
123
124 Token::StringMulti(multi) => {
125 Some(Datatype::Variant(Variant::String(multi.content.to_string())))
126 }
127
128 Token::RbxAsset(slice) => {
129 Some(Datatype::Variant(Variant::String(slice.to_string())))
130 }
131
132 Token::RbxContent(slice) => {
133 Some(Datatype::Variant(Variant::Content(Content::from(
134 slice.to_string(),
135 ))))
136 }
137
138 Token::Boolean(s) => {
139 let val = *s == "true";
140 Some(Datatype::Variant(Variant::Bool(val)))
141 }
142
143 Token::Nil => Some(Datatype::None),
144
145 Token::ColorHex(slice) => {
146 let hex = normalize_hex(slice);
147 let color: Result<Srgb<u8>, _> = hex.parse();
148 color.ok().map(|c| {
149 Datatype::Variant(Variant::Color3(
150 Color3uint8::new(c.red, c.green, c.blue).into(),
151 ))
152 })
153 }
154
155 Token::ColorTailwind(slice) => {
156 TAILWIND_COLORS
157 .get(&slice.to_lowercase())
158 .map(|color| Datatype::Oklab(***color))
159 }
160
161 Token::ColorSkin(slice) => {
162 SKIN_COLORS
163 .get(&slice.to_lowercase())
164 .map(|color| Datatype::Oklab(***color))
165 }
166
167 Token::ColorCss(slice) => {
168 CSS_COLORS
169 .get(&slice.to_lowercase())
170 .map(|color| Datatype::Oklab(***color))
171 }
172
173 Token::ColorBrick(slice) => {
174 BRICK_COLORS
175 .get(&slice.to_lowercase())
176 .map(|color| Datatype::Oklab(***color))
177 }
178
179 Token::TokenIdentifier(attr_name) => Some(lookup.resolve_dynamic(attr_name)),
180
181 Token::StaticTokenIdentifier(static_name) => Some(lookup.resolve_static(static_name)),
182
183 Token::MacroArgIdentifier(Some(name)) => lookup.resolve_macro_arg(name, key),
184
185 Token::StateSelectorOrEnumPart(Some(value)) => {
186 if let Some(key) = key {
187 let rebinded_key = shorthand_rebind(key);
188 EnumItem::from_name_and_value_name(rebinded_key, value)
189 .map(|item| Datatype::Variant(Variant::EnumItem(item)))
190 .or(Some(Datatype::None))
191 } else {
192 Some(Datatype::IncompleteEnumShorthand(value.to_string()))
193 }
194 }
195
196 _ => None,
197 }
198}
199
200fn evaluate_delimited_to_vec(
201 delimited: &Delimited,
202 lookup: &dyn StaticLookup,
203) -> Vec<Datatype> {
204 let Some(content) = &delimited.content else {
205 return vec![];
206 };
207
208 content
209 .iter()
210 .filter_map(|c| evaluate_construct(c, None, lookup))
211 .collect()
212}
213
214fn coerce_tuple_data(datatypes: Vec<Datatype>, name: Option<&str>) -> Option<Datatype> {
215 let mut t = tuple::Tuple::new(name.map(|s| s.to_string()));
216 for d in datatypes {
217 t.push(d);
218 }
219 let result = t.coerce_to_datatype();
220 match result {
221 Datatype::None => None,
222 other => Some(other),
223 }
224}
225
226fn negate_variant(variant: &Variant) -> Option<Variant> {
227 match variant {
228 Variant::Float64(n) => Some(Variant::Float64(-n)),
229 Variant::UDim(udim) => Some(Variant::UDim(UDim::new(-udim.scale, -udim.offset))),
230 _ => None,
231 }
232}
233
234fn apply_operator(op_node: &Node, left: &Variant, right: &Variant) -> Option<Variant> {
235 let narrowed_right;
236 let right = match (left, right) {
237 (Variant::Float64(_), _) => right,
238 (_, Variant::Float64(n)) => {
239 narrowed_right = Variant::Float32(*n as f32);
240 &narrowed_right
241 }
242 _ => right,
243 };
244
245 match op_node.token.value() {
246 Token::OpAdd => left.add(right),
247 Token::OpSub => left.sub(right),
248 Token::OpMult => left.mult(right),
249 Token::OpDiv => left.div(right),
250 Token::OpFloorDiv => left.floor_div(right),
251 Token::OpMod => left.modulus(right),
252 Token::OpPow => left.pow(right),
253 _ => None,
254 }
255}
256
257fn normalize_hex(hex: &str) -> String {
258 let hex = hex.trim_start_matches('#');
259 match hex.len() {
260 3 | 6 => hex.into(),
261 1..=5 => format!("{:0<6}", hex),
262 _ => hex.into(),
263 }
264}
265
266const SHORTHAND_REBINDS: phf::Map<&'static str, &'static str> = phf_macros::phf_map! {
267 "FlexMode" => "UIFlexMode",
268 "HorizontalFlex" => "UIFlexAlignment",
269 "VerticalFlex" => "UIFlexAlignment",
270};
271
272pub(crate) fn shorthand_rebind<'a>(key: &'a str) -> &'a str {
273 SHORTHAND_REBINDS.get(key).copied().unwrap_or(key)
274}
275
276fn parse_number_str(s: &str) -> Option<f64> {
277 let cleaned: String = s.chars().filter(|c| *c != '_').collect();
278 cleaned.parse::<f64>().ok()
279}