1use crate::format::Opcode;
8use crate::parser::IrModule;
9use crate::walker::{read_tag_with_attrs, read_u16, read_u32};
10use std::fmt::Write;
11
12pub fn dump_ir(module: &IrModule) -> String {
18 let mut out = String::with_capacity(module.opcodes.len() * 8);
19
20 writeln!(
22 out,
23 "FMIR v{} source_hash={:016x} strings={} slots={} islands={}",
24 module.header.version,
25 module.header.source_hash,
26 module.strings.len(),
27 module.slots.len(),
28 module.islands.len(),
29 )
30 .unwrap();
31
32 let ops = &module.opcodes;
34 let strings = &module.strings;
35 let len = ops.len();
36 let mut pos: usize = 0;
37
38 while pos < len {
39 let offset = pos;
40 let opcode = match Opcode::from_byte(ops[pos]) {
41 Ok(op) => op,
42 Err(_) => {
43 writeln!(out, "{:04x}: UNKNOWN 0x{:02x}", offset, ops[pos]).unwrap();
44 pos += 1;
45 continue;
46 }
47 };
48 pos += 1; match opcode {
51 Opcode::OpenTag => {
52 let (tag_str_idx, attrs, new_pos) = match read_tag_with_attrs(ops, pos, strings) {
53 Ok(v) => v,
54 Err(e) => {
55 writeln!(out, "{:04x}: OPEN_TAG <error: {}>", offset, e).unwrap();
56 break;
57 }
58 };
59 let tag = strings.get(tag_str_idx).unwrap_or("?");
60 write!(
61 out,
62 "{:04x}: OPEN_TAG \"{}\" attrs={}",
63 offset,
64 tag,
65 attrs.len()
66 )
67 .unwrap();
68 if !attrs.is_empty() {
69 write!(out, " [").unwrap();
70 for (i, (key, val)) in attrs.iter().enumerate() {
71 if i > 0 {
72 write!(out, ", ").unwrap();
73 }
74 write!(out, "(\"{}\",\"{}\")", key, val).unwrap();
75 }
76 write!(out, "]").unwrap();
77 }
78 writeln!(out).unwrap();
79 pos = new_pos;
80 }
81
82 Opcode::CloseTag => {
83 let str_idx = match read_u32(ops, pos) {
84 Ok(v) => v,
85 Err(e) => {
86 writeln!(out, "{:04x}: CLOSE_TAG <error: {}>", offset, e).unwrap();
87 break;
88 }
89 };
90 pos += 4;
91 let tag = strings.get(str_idx).unwrap_or("?");
92 writeln!(out, "{:04x}: CLOSE_TAG \"{}\"", offset, tag).unwrap();
93 }
94
95 Opcode::VoidTag => {
96 let (tag_str_idx, attrs, new_pos) = match read_tag_with_attrs(ops, pos, strings) {
97 Ok(v) => v,
98 Err(e) => {
99 writeln!(out, "{:04x}: VOID_TAG <error: {}>", offset, e).unwrap();
100 break;
101 }
102 };
103 let tag = strings.get(tag_str_idx).unwrap_or("?");
104 write!(
105 out,
106 "{:04x}: VOID_TAG \"{}\" attrs={}",
107 offset,
108 tag,
109 attrs.len()
110 )
111 .unwrap();
112 if !attrs.is_empty() {
113 write!(out, " [").unwrap();
114 for (i, (key, val)) in attrs.iter().enumerate() {
115 if i > 0 {
116 write!(out, ", ").unwrap();
117 }
118 write!(out, "(\"{}\",\"{}\")", key, val).unwrap();
119 }
120 write!(out, "]").unwrap();
121 }
122 writeln!(out).unwrap();
123 pos = new_pos;
124 }
125
126 Opcode::Text => {
127 let str_idx = match read_u32(ops, pos) {
128 Ok(v) => v,
129 Err(e) => {
130 writeln!(out, "{:04x}: TEXT <error: {}>", offset, e).unwrap();
131 break;
132 }
133 };
134 pos += 4;
135 let text = strings.get(str_idx).unwrap_or("?");
136 writeln!(out, "{:04x}: TEXT \"{}\"", offset, text).unwrap();
137 }
138
139 Opcode::DynText => {
140 let slot_id = match read_u16(ops, pos) {
142 Ok(v) => v,
143 Err(e) => {
144 writeln!(out, "{:04x}: DYN_TEXT <error: {}>", offset, e).unwrap();
145 break;
146 }
147 };
148 let marker_id = match read_u16(ops, pos + 2) {
149 Ok(v) => v,
150 Err(e) => {
151 writeln!(out, "{:04x}: DYN_TEXT <error: {}>", offset, e).unwrap();
152 break;
153 }
154 };
155 pos += 4;
156 writeln!(
157 out,
158 "{:04x}: DYN_TEXT slot={} marker=t{}",
159 offset, slot_id, marker_id
160 )
161 .unwrap();
162 }
163
164 Opcode::DynAttr => {
165 let attr_str_idx = match read_u32(ops, pos) {
167 Ok(v) => v,
168 Err(e) => {
169 writeln!(out, "{:04x}: DYN_ATTR <error: {}>", offset, e).unwrap();
170 break;
171 }
172 };
173 let slot_id = match read_u16(ops, pos + 4) {
174 Ok(v) => v,
175 Err(e) => {
176 writeln!(out, "{:04x}: DYN_ATTR <error: {}>", offset, e).unwrap();
177 break;
178 }
179 };
180 pos += 6;
181 let attr_name = strings.get(attr_str_idx).unwrap_or("?");
182 writeln!(
183 out,
184 "{:04x}: DYN_ATTR \"{}\" slot={}",
185 offset, attr_name, slot_id
186 )
187 .unwrap();
188 }
189
190 Opcode::ShowIf => {
191 let slot_id = match read_u16(ops, pos) {
193 Ok(v) => v,
194 Err(e) => {
195 writeln!(out, "{:04x}: SHOW_IF <error: {}>", offset, e).unwrap();
196 break;
197 }
198 };
199 let then_len = match read_u32(ops, pos + 2) {
200 Ok(v) => v,
201 Err(e) => {
202 writeln!(out, "{:04x}: SHOW_IF <error: {}>", offset, e).unwrap();
203 break;
204 }
205 };
206 let else_len = match read_u32(ops, pos + 6) {
207 Ok(v) => v,
208 Err(e) => {
209 writeln!(out, "{:04x}: SHOW_IF <error: {}>", offset, e).unwrap();
210 break;
211 }
212 };
213 pos += 10;
214 writeln!(
215 out,
216 "{:04x}: SHOW_IF slot={} then_len={} else_len={}",
217 offset, slot_id, then_len, else_len
218 )
219 .unwrap();
220 }
221
222 Opcode::ShowElse => {
223 writeln!(out, "{:04x}: SHOW_ELSE", offset).unwrap();
225 }
226
227 Opcode::Switch => {
228 let slot_id = match read_u16(ops, pos) {
230 Ok(v) => v,
231 Err(e) => {
232 writeln!(out, "{:04x}: SWITCH <error: {}>", offset, e).unwrap();
233 break;
234 }
235 };
236 let case_count = match read_u16(ops, pos + 2) {
237 Ok(v) => v,
238 Err(e) => {
239 writeln!(out, "{:04x}: SWITCH <error: {}>", offset, e).unwrap();
240 break;
241 }
242 };
243 pos += 4;
244 pos += (case_count as usize) * 8;
246 writeln!(
247 out,
248 "{:04x}: SWITCH slot={} cases={}",
249 offset, slot_id, case_count
250 )
251 .unwrap();
252 }
253
254 Opcode::List => {
255 let slot_id = match read_u16(ops, pos) {
257 Ok(v) => v,
258 Err(e) => {
259 writeln!(out, "{:04x}: LIST <error: {}>", offset, e).unwrap();
260 break;
261 }
262 };
263 let item_slot_id = match read_u16(ops, pos + 2) {
264 Ok(v) => v,
265 Err(e) => {
266 writeln!(out, "{:04x}: LIST <error: {}>", offset, e).unwrap();
267 break;
268 }
269 };
270 let body_len = match read_u32(ops, pos + 4) {
271 Ok(v) => v,
272 Err(e) => {
273 writeln!(out, "{:04x}: LIST <error: {}>", offset, e).unwrap();
274 break;
275 }
276 };
277 pos += 8;
278 writeln!(
279 out,
280 "{:04x}: LIST slot={} item_slot={} body_len={}",
281 offset, slot_id, item_slot_id, body_len
282 )
283 .unwrap();
284 }
285
286 Opcode::IslandStart => {
287 let island_id = match read_u16(ops, pos) {
289 Ok(v) => v,
290 Err(e) => {
291 writeln!(out, "{:04x}: ISLAND_START <error: {}>", offset, e).unwrap();
292 break;
293 }
294 };
295 pos += 2;
296 writeln!(out, "{:04x}: ISLAND_START id={}", offset, island_id).unwrap();
297 }
298
299 Opcode::IslandEnd => {
300 let island_id = match read_u16(ops, pos) {
302 Ok(v) => v,
303 Err(e) => {
304 writeln!(out, "{:04x}: ISLAND_END <error: {}>", offset, e).unwrap();
305 break;
306 }
307 };
308 pos += 2;
309 writeln!(out, "{:04x}: ISLAND_END id={}", offset, island_id).unwrap();
310 }
311
312 Opcode::TryStart => {
313 let fallback_len = match read_u32(ops, pos) {
315 Ok(v) => v,
316 Err(e) => {
317 writeln!(out, "{:04x}: TRY_START <error: {}>", offset, e).unwrap();
318 break;
319 }
320 };
321 pos += 4;
322 writeln!(
323 out,
324 "{:04x}: TRY_START fallback_len={}",
325 offset, fallback_len
326 )
327 .unwrap();
328 }
329
330 Opcode::Fallback => {
331 writeln!(out, "{:04x}: FALLBACK", offset).unwrap();
333 }
334
335 Opcode::Preload => {
336 if pos >= ops.len() {
338 writeln!(
339 out,
340 "{:04x}: PRELOAD <error: buffer too short>",
341 offset
342 )
343 .unwrap();
344 break;
345 }
346 let resource_type = ops[pos];
347 let url_str_idx = match read_u32(ops, pos + 1) {
348 Ok(v) => v,
349 Err(e) => {
350 writeln!(out, "{:04x}: PRELOAD <error: {}>", offset, e).unwrap();
351 break;
352 }
353 };
354 pos += 5;
355 let url = strings.get(url_str_idx).unwrap_or("?");
356 writeln!(
357 out,
358 "{:04x}: PRELOAD type={} url=\"{}\"",
359 offset, resource_type, url
360 )
361 .unwrap();
362 }
363
364 Opcode::Comment => {
365 let str_idx = match read_u32(ops, pos) {
367 Ok(v) => v,
368 Err(e) => {
369 writeln!(out, "{:04x}: COMMENT <error: {}>", offset, e).unwrap();
370 break;
371 }
372 };
373 pos += 4;
374 let text = strings.get(str_idx).unwrap_or("?");
375 writeln!(out, "{:04x}: COMMENT \"{}\"", offset, text).unwrap();
376 }
377
378 Opcode::ListItemKey => {
379 let str_idx = match read_u32(ops, pos) {
381 Ok(v) => v,
382 Err(e) => {
383 writeln!(out, "{:04x}: LIST_ITEM_KEY <error: {}>", offset, e).unwrap();
384 break;
385 }
386 };
387 pos += 4;
388 let key = strings.get(str_idx).unwrap_or("?");
389 writeln!(out, "{:04x}: LIST_ITEM_KEY \"{}\"", offset, key).unwrap();
390 }
391
392 Opcode::Prop => {
393 let src = match read_u16(ops, pos) {
395 Ok(v) => v,
396 Err(e) => {
397 writeln!(out, "{:04x}: PROP <error: {}>", offset, e).unwrap();
398 break;
399 }
400 };
401 let prop_idx = match read_u32(ops, pos + 2) {
402 Ok(v) => v,
403 Err(e) => {
404 writeln!(out, "{:04x}: PROP <error: {}>", offset, e).unwrap();
405 break;
406 }
407 };
408 let target = match read_u16(ops, pos + 6) {
409 Ok(v) => v,
410 Err(e) => {
411 writeln!(out, "{:04x}: PROP <error: {}>", offset, e).unwrap();
412 break;
413 }
414 };
415 pos += 8;
416 let prop_name = strings.get(prop_idx).unwrap_or("?");
417 writeln!(
418 out,
419 "{:04x}: PROP slot[{}].\"{}\" -> slot[{}]",
420 offset, src, prop_name, target
421 )
422 .unwrap();
423 }
424 }
425 }
426
427 out
428}
429
430#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::parser::test_helpers::{
438 build_minimal_ir, encode_close_tag, encode_open_tag, encode_text, encode_void_tag,
439 };
440
441 fn encode_dyn_text(slot_id: u16, marker_id: u16) -> Vec<u8> {
445 let mut buf = Vec::new();
446 buf.push(0x05); buf.extend_from_slice(&slot_id.to_le_bytes());
448 buf.extend_from_slice(&marker_id.to_le_bytes());
449 buf
450 }
451
452 fn dump_static(strings: &[&str], opcodes: &[u8]) -> String {
454 let data = build_minimal_ir(strings, &[], opcodes, &[]);
455 let module = IrModule::parse(&data).unwrap();
456 dump_ir(&module)
457 }
458
459 #[test]
462 fn dump_static_div() {
463 let mut opcodes = Vec::new();
466 opcodes.extend_from_slice(&encode_open_tag(0, &[(1, 2)]));
467 opcodes.extend_from_slice(&encode_text(3));
468 opcodes.extend_from_slice(&encode_close_tag(0));
469
470 let output = dump_static(&["div", "class", "container", "Hello"], &opcodes);
471
472 assert!(output.contains("OPEN_TAG"), "should contain OPEN_TAG");
473 assert!(output.contains("\"div\""), "should contain tag name");
474 assert!(output.contains("\"class\""), "should contain attr key");
475 assert!(output.contains("\"container\""), "should contain attr val");
476 assert!(output.contains("TEXT"), "should contain TEXT");
477 assert!(output.contains("\"Hello\""), "should contain text content");
478 assert!(output.contains("CLOSE_TAG"), "should contain CLOSE_TAG");
479 }
480
481 #[test]
484 fn dump_with_dyn_text() {
485 let opcodes = encode_dyn_text(0, 0);
489
490 let data = build_minimal_ir(&["greeting"], &[(0, 0, 0x01, 0x00, &[])], &opcodes, &[]);
491 let module = IrModule::parse(&data).unwrap();
492 let output = dump_ir(&module);
493
494 assert!(output.contains("DYN_TEXT"), "should contain DYN_TEXT");
495 assert!(output.contains("slot=0"), "should contain slot=0");
496 assert!(output.contains("marker=t0"), "should contain marker=t0");
497 }
498
499 #[test]
502 fn dump_header_line() {
503 let opcodes = encode_text(0);
504 let output = dump_static(&["Hello"], &opcodes);
505
506 let first_line = output.lines().next().unwrap();
507 assert!(
508 first_line.starts_with("FMIR v2"),
509 "first line should start with FMIR v2, got: {}",
510 first_line
511 );
512 assert!(
513 first_line.contains("source_hash="),
514 "first line should contain source_hash="
515 );
516 assert!(
517 first_line.contains("strings="),
518 "first line should contain strings="
519 );
520 assert!(
521 first_line.contains("slots="),
522 "first line should contain slots="
523 );
524 assert!(
525 first_line.contains("islands="),
526 "first line should contain islands="
527 );
528 }
529
530 #[test]
533 fn dump_deterministic() {
534 let mut opcodes = Vec::new();
535 opcodes.extend_from_slice(&encode_open_tag(0, &[(1, 2)]));
536 opcodes.extend_from_slice(&encode_text(3));
537 opcodes.extend_from_slice(&encode_close_tag(0));
538
539 let data = build_minimal_ir(&["div", "class", "container", "Hello"], &[], &opcodes, &[]);
540 let module = IrModule::parse(&data).unwrap();
541
542 let dump1 = dump_ir(&module);
543 let dump2 = dump_ir(&module);
544
545 assert_eq!(
546 dump1, dump2,
547 "same IR dumped twice must produce identical output"
548 );
549 }
550
551 #[test]
554 fn dump_void_tag() {
555 let opcodes = encode_void_tag(0, &[(1, 2)]);
558 let output = dump_static(&["input", "type", "email"], &opcodes);
559
560 assert!(output.contains("VOID_TAG"), "should contain VOID_TAG");
561 assert!(output.contains("\"input\""), "should contain tag name");
562 assert!(output.contains("\"type\""), "should contain attr key");
563 assert!(output.contains("\"email\""), "should contain attr val");
564 }
565}