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