1use plotnik_bytecode::{Module, TypeId};
7#[cfg(debug_assertions)]
8use plotnik_bytecode::{StringsView, TypeData, TypeKind, TypesView};
9use plotnik_core::Colors;
10
11use super::Value;
12
13#[cfg(debug_assertions)]
20pub fn debug_verify_type(value: &Value, declared_type: TypeId, module: &Module, colors: Colors) {
21 let types = module.types();
22 let strings = module.strings();
23
24 let mut errors = Vec::new();
25 verify_type(
26 value,
27 declared_type,
28 &types,
29 &strings,
30 &mut String::new(),
31 &mut errors,
32 );
33 if !errors.is_empty() {
34 panic_with_mismatch(value, declared_type, &errors, module, colors);
35 }
36}
37
38#[cfg(not(debug_assertions))]
40#[inline(always)]
41pub fn debug_verify_type(
42 _value: &Value,
43 _declared_type: TypeId,
44 _module: &Module,
45 _colors: Colors,
46) {
47}
48
49#[cfg(debug_assertions)]
51fn verify_type(
52 value: &Value,
53 declared: TypeId,
54 types: &TypesView<'_>,
55 strings: &StringsView<'_>,
56 path: &mut String,
57 errors: &mut Vec<String>,
58) {
59 let Some(type_def) = types.get(declared) else {
60 errors.push(format_error(
61 path,
62 &format!("unknown type id {}", declared.0),
63 ));
64 return;
65 };
66
67 match type_def.classify() {
68 TypeData::Primitive(kind) => match kind {
69 TypeKind::Void => {
70 if !matches!(value, Value::Null) {
71 errors.push(format_error(
72 path,
73 &format!("type: void, value: {}", value_kind_name(value)),
74 ));
75 }
76 }
77 TypeKind::Node => {
78 if !matches!(value, Value::Node(_)) {
79 errors.push(format_error(
80 path,
81 &format!("type: Node, value: {}", value_kind_name(value)),
82 ));
83 }
84 }
85 TypeKind::String => {
86 if !matches!(value, Value::String(_)) {
87 errors.push(format_error(
88 path,
89 &format!("type: string, value: {}", value_kind_name(value)),
90 ));
91 }
92 }
93 _ => unreachable!(),
94 },
95
96 TypeData::Wrapper { kind, inner } => match kind {
97 TypeKind::Alias => {
98 if !matches!(value, Value::Node(_)) {
99 errors.push(format_error(
100 path,
101 &format!("type: Node (alias), value: {}", value_kind_name(value)),
102 ));
103 }
104 }
105 TypeKind::Optional => {
106 if !matches!(value, Value::Null) {
107 verify_type(value, inner, types, strings, path, errors);
108 }
109 }
110 TypeKind::ArrayZeroOrMore => match value {
111 Value::Array(items) => {
112 for (i, item) in items.iter().enumerate() {
113 let prev_len = path.len();
114 path.push_str(&format!("[{}]", i));
115 verify_type(item, inner, types, strings, path, errors);
116 path.truncate(prev_len);
117 }
118 }
119 _ => {
120 errors.push(format_error(
121 path,
122 &format!("type: array, value: {}", value_kind_name(value)),
123 ));
124 }
125 },
126 TypeKind::ArrayOneOrMore => match value {
127 Value::Array(items) => {
128 if items.is_empty() {
129 errors.push(format_error(
130 path,
131 "type: non-empty array, value: empty array",
132 ));
133 }
134 for (i, item) in items.iter().enumerate() {
135 let prev_len = path.len();
136 path.push_str(&format!("[{}]", i));
137 verify_type(item, inner, types, strings, path, errors);
138 path.truncate(prev_len);
139 }
140 }
141 _ => {
142 errors.push(format_error(
143 path,
144 &format!("type: array, value: {}", value_kind_name(value)),
145 ));
146 }
147 },
148 _ => unreachable!(),
149 },
150
151 TypeData::Composite { kind, .. } => match kind {
152 TypeKind::Struct => match value {
153 Value::Object(fields) => {
154 for member in types.members_of(&type_def) {
155 let field_name = strings.get(member.name);
156 let (inner_type, is_optional) = types.unwrap_optional(member.type_id);
157
158 let field_value = fields.iter().find(|(k, _)| k == field_name);
159 match field_value {
160 Some((_, v)) => {
161 if is_optional && matches!(v, Value::Null) {
162 continue;
163 }
164 let prev_len = path.len();
165 path.push('.');
166 path.push_str(field_name);
167 verify_type(v, inner_type, types, strings, path, errors);
168 path.truncate(prev_len);
169 }
170 None => {
171 if !is_optional {
172 errors.push(format!(
173 "{}: required field missing",
174 append_path(path, field_name)
175 ));
176 }
177 }
178 }
179 }
180 }
181 _ => {
182 errors.push(format_error(
183 path,
184 &format!("type: object, value: {}", value_kind_name(value)),
185 ));
186 }
187 },
188 TypeKind::Enum => match value {
189 Value::Tagged { tag, data } => {
190 let variant = types
191 .members_of(&type_def)
192 .find(|m| strings.get(m.name) == tag);
193
194 match variant {
195 Some(member) => {
196 let is_void = types.get(member.type_id).is_some_and(|d| {
197 matches!(d.classify(), TypeData::Primitive(TypeKind::Void))
198 });
199
200 if is_void {
201 if data.is_some() {
202 errors.push(format!(
203 "{}: void variant '{}' should have no $data",
204 append_path(path, "$data"),
205 tag
206 ));
207 }
208 } else {
209 match data {
210 Some(d) => {
211 let prev_len = path.len();
212 path.push_str(".$data");
213 verify_type(
214 d,
215 member.type_id,
216 types,
217 strings,
218 path,
219 errors,
220 );
221 path.truncate(prev_len);
222 }
223 None => {
224 errors.push(format!(
225 "{}: non-void variant '{}' should have $data",
226 append_path(path, "$data"),
227 tag
228 ));
229 }
230 }
231 }
232 }
233 None => {
234 errors.push(format!(
235 "{}: unknown variant '{}'",
236 append_path(path, "$tag"),
237 tag
238 ));
239 }
240 }
241 }
242 _ => {
243 errors.push(format_error(
244 path,
245 &format!("type: tagged union, value: {}", value_kind_name(value)),
246 ));
247 }
248 },
249 _ => unreachable!(),
250 },
251 }
252}
253
254#[cfg(debug_assertions)]
256fn value_kind_name(value: &Value) -> &'static str {
257 match value {
258 Value::Null => "null",
259 Value::String(_) => "string",
260 Value::Node(_) => "Node",
261 Value::Array(_) => "array",
262 Value::Object(_) => "object",
263 Value::Tagged { .. } => "tagged union",
264 }
265}
266
267#[cfg(debug_assertions)]
269fn format_path(path: &str) -> String {
270 path.strip_prefix('.').unwrap_or(path).to_string()
271}
272
273#[cfg(debug_assertions)]
275fn format_error(path: &str, msg: &str) -> String {
276 let p = format_path(path);
277 if p.is_empty() {
278 msg.to_string()
279 } else {
280 format!("{}: {}", p, msg)
281 }
282}
283
284#[cfg(debug_assertions)]
286fn append_path(path: &str, suffix: &str) -> String {
287 let p = format_path(path);
288 if p.is_empty() {
289 suffix.to_string()
290 } else {
291 format!("{}.{}", p, suffix)
292 }
293}
294
295#[cfg(debug_assertions)]
297fn centered_header(label: &str, width: usize) -> String {
298 let label_with_spaces = format!(" {} ", label);
299 let label_len = label_with_spaces.len();
300 if label_len >= width {
301 return label_with_spaces;
302 }
303 let remaining = width - label_len;
304 let left = remaining / 2;
305 let right = remaining - left;
306 format!(
307 "{}{}{}",
308 "-".repeat(left),
309 label_with_spaces,
310 "-".repeat(right)
311 )
312}
313
314#[cfg(debug_assertions)]
316fn panic_with_mismatch(
317 value: &Value,
318 declared_type: TypeId,
319 errors: &[String],
320 module: &Module,
321 colors: Colors,
322) -> ! {
323 const WIDTH: usize = 80;
324 let separator = "=".repeat(WIDTH);
325
326 let entrypoints = module.entrypoints();
327 let strings = module.strings();
328
329 let type_name = (0..entrypoints.len())
331 .find_map(|i| {
332 let e = entrypoints.get(i);
333 if e.result_type() == declared_type {
334 Some(strings.get(e.name()))
335 } else {
336 None
337 }
338 })
339 .unwrap_or("unknown");
340
341 let value_str = value.format(true, colors);
342 let details_str = errors.join("\n");
343
344 let output_header = centered_header(&format!("Output: {}", type_name), WIDTH);
345 let details_header = centered_header("Details", WIDTH);
346
347 panic!(
348 "\n{separator}\n\
349 BUG: Type and value do not match\n\
350 {separator}\n\n\
351 {output_header}\n\n\
352 {value_str}\n\n\
353 {details_header}\n\n\
354 {details_str}\n\n\
355 {separator}\n"
356 );
357}