Skip to main content

tycho_disasm/
lib.rs

1use std::collections::hash_map;
2use std::fmt::Write;
3
4use ahash::HashMap;
5use anyhow::Result;
6use num_bigint::BigInt;
7use smol_str::SmolStr;
8use tycho_types::prelude::*;
9use tycho_vm::{DumpError, DumpOutput, DumpResult};
10
11pub use self::models::{
12    Code, CodeBlock, CodeBlockTail, Data, DataBlock, Item, ItemId, JumpTable, Library, Opcode,
13    OpcodeArg,
14};
15
16mod models;
17mod tokenizer;
18
19pub fn disasm_structured(code: Cell) -> Result<Code> {
20    let mut stack = Vec::new();
21    let mut res = Resources::default();
22
23    // Add root continuation.
24    let root = match res.add_cont(code) {
25        // Root continuation is a code so add it to the stack to process.
26        Ok(item) => {
27            let id = item.id;
28            stack.push(item);
29            id
30        }
31        // Root continuation is a library so just use its id.
32        Err(id) => id,
33    };
34
35    // Disasm works only with CP0 for now.
36    let cp = tycho_vm::codepage0();
37
38    // Prepare dump state which will be filled by `dispatch_dump`.
39    let mut state = DumpState {
40        opcode: String::with_capacity(128),
41        ..Default::default()
42    };
43
44    // New continuations are collected during the instruction dump
45    // and added to the stack afterwards.
46    let mut new_conts = Vec::new();
47    let mut links = Vec::new();
48
49    'outer: while let Some(item) = stack.last_mut() {
50        // Process the topmost continuation.
51
52        // Apply cell slice parts to get a `CellSlice` with code.
53        let mut cs = item.slice.0.apply(&item.slice.1)?;
54
55        // Use the current code root in a DumpState so it can transform
56        // `CellSlice<'_>` into `CellSliceParts`.
57        state.root = item.slice.1.clone();
58
59        let mut incomplete = false;
60        'inner: while !cs.is_data_empty() {
61            // Remember the current position relative to the code cell.
62            let offset_bits = cs.offset_bits();
63            let offset_refs = cs.offset_refs();
64
65            // Dispatch one instruction and dump its info into the `DumpState`.
66            let dump = cp.dispatch_dump(&mut cs, &mut state);
67            // NOTE: Code root cell must not change.
68            debug_assert_eq!(cs.cell(), item.slice.1.as_ref());
69
70            match dump {
71                // Apply the new slice range to the stack item.
72                Ok(()) => item.slice.0 = cs.range(),
73                // In case of an explicitly invalid opcode treat the remainder as some unparsed tail.
74                Err(DumpError::InvalidOpcode) => {
75                    state.clear();
76                    incomplete = true;
77                    break 'inner;
78                }
79                // All other errors are fatal.
80                Err(e) => return Err(e.into()),
81            }
82
83            // Fill opcode links to cells (as data).
84            for cell in state.cells.drain(..) {
85                let id = res.add_cell(cell);
86                links.push(id);
87            }
88
89            // Fill opcode links to slices (as data).
90            for slice in state.slices.drain(..) {
91                let id = res.add_slice(slice);
92                links.push(id);
93            }
94
95            // Fill opcode links to continuation references.
96            for cont in state.conts.drain(..) {
97                let id = match res.add_cont(cont) {
98                    // New continuation found, remember to push it to the stack
99                    // right after this opcode is processed.
100                    Ok(item) => {
101                        let id = item.id;
102                        new_conts.push(item);
103                        id
104                    }
105                    // An existing continuation found, just add it as link.
106                    Err(id) => id,
107                };
108
109                links.push(id);
110            }
111
112            // Fill opcode links to inline continuations.
113            for cont in state.cont_slices.drain(..) {
114                let id = match res.add_cont_slice(cont) {
115                    // New continuation found, remember to push it to the stack
116                    // right after this opcode is processed.
117                    Ok(item) => {
118                        let id = item.id;
119                        new_conts.push(item);
120                        id
121                    }
122                    // An existing continuation found, just add it as link.
123                    Err(id) => id,
124                };
125
126                links.push(id);
127            }
128
129            // Fill opcode links to jump tables.
130            for (n, root) in state.dicts.drain(..) {
131                let id = match res.add_jump_table(n, root) {
132                    // New jump table found, remember to push its items to the stack
133                    // right after this opcode is processed.
134                    Ok(item) => {
135                        let id = item.id;
136                        new_conts.extend(item.to_parse);
137                        id
138                    }
139                    // An existing jump table found, just add it as link.
140                    Err(id) => id,
141                };
142
143                links.push(id);
144            }
145
146            // Tokenize opcode text.
147            let (name, args) = res.tokenize(&state.opcode, &links)?;
148
149            // Add opcode to the code block.
150            item.block.opcodes.push(Opcode {
151                bits: cs.offset_bits() - offset_bits,
152                refs: cs.offset_refs() - offset_refs,
153                name,
154                args,
155                gas: state.gas,
156            });
157
158            // Reset `DumpState`.
159            state.clear();
160            links.clear();
161
162            // Refill the stack with new continuations.
163            // NOTE: Reverse order is used here to preserve the resulting total ordering.
164            if !new_conts.is_empty() {
165                stack.extend(new_conts.drain(..).rev());
166                continue 'outer;
167            }
168        }
169
170        // Try to get the next cell if the code was fully processed.
171        let next = (!incomplete && !cs.is_refs_empty())
172            .then(|| cs.load_reference_cloned())
173            .transpose()?;
174
175        // This stack item is not needed on stack anymore.
176        let mut item = stack.pop().unwrap();
177
178        if let Some(next) = next {
179            // Try to add the next cell as a continuation.
180            match res.add_cont(next) {
181                // New continuation found, link it to the previous and push it to the stack.
182                Ok(next_item) => {
183                    item.block.tail = Some(CodeBlockTail::Child { id: next_item.id });
184                    stack.push(next_item);
185                }
186                // An existing continuation found, just link it to the previous.
187                Err(next_id) => {
188                    item.block.tail = Some(CodeBlockTail::Child { id: next_id });
189                }
190            }
191        } else if incomplete {
192            // If code was not fully processed mark the block tail as incomplete.
193            item.block.tail = Some(CodeBlockTail::Incomplete);
194        } else {
195            // No more code left in this continuation.
196            item.block.tail = None;
197        }
198
199        // Replace the stub with the code block.
200        res.items[item.id as usize] = Item::Code(item.block);
201
202        // Save this code block to the jump table if needed.
203        if let Some(save_to) = item.save_to {
204            let Item::JumpTable(table) = &mut res.items[save_to.jump_table_id as usize] else {
205                unreachable!("save_to index mismatch");
206            };
207            table.items.push((save_to.key, item.id));
208        }
209    }
210
211    // Done.
212    Ok(Code {
213        root,
214        items: res.items,
215    })
216}
217
218#[derive(Default)]
219struct Resources {
220    cont_to_id: HashMap<HashBytes, ItemId>,
221    cont_slice_to_id: HashMap<SliceKey, ItemId>,
222    cell_to_id: HashMap<HashBytes, ItemId>,
223    slice_to_id: HashMap<SliceKey, ItemId>,
224    lib_to_id: HashMap<HashBytes, ItemId>,
225    dict_to_id: HashMap<(u16, HashBytes), ItemId>,
226    items: Vec<Item>,
227}
228
229impl Resources {
230    fn tokenize(&self, text: &str, links: &[ItemId]) -> Result<(SmolStr, Vec<OpcodeArg>)> {
231        use self::tokenizer::{OpcodeArgToken, OpcodeTokens, tokenize};
232
233        let OpcodeTokens { name, args } = tokenize(text)?;
234
235        let mut links = links.iter();
236        let mut res = Vec::with_capacity(args.len());
237        for arg in args {
238            res.push(match arg {
239                OpcodeArgToken::Int(int) => OpcodeArg::Int(int),
240                OpcodeArgToken::Stack { idx } => OpcodeArg::Stack { idx },
241                OpcodeArgToken::Reg { idx } => OpcodeArg::Reg { idx },
242                OpcodeArgToken::Cell => {
243                    let Some(id) = links.next().copied() else {
244                        anyhow::bail!("not enough cell links");
245                    };
246
247                    match self.items.get(id as usize) {
248                        // Accept links to data cells.
249                        Some(Item::Data(DataBlock {
250                            data: Data::Cell(..),
251                        })) => {}
252                        // Accept links to non-inlined continuations.
253                        Some(Item::Code(code)) => {
254                            anyhow::ensure!(
255                                !code.is_inline,
256                                "cell argument points to an inlined continuation"
257                            );
258                        }
259                        // Accept libraries.
260                        Some(Item::Library(..)) => {}
261                        // Reject other types.
262                        Some(_) => anyhow::bail!("invalid link type"),
263                        // Unknown link.
264                        None => anyhow::bail!("unknown link"),
265                    }
266
267                    OpcodeArg::Cell { id }
268                }
269                OpcodeArgToken::Slice => {
270                    let Some(id) = links.next().copied() else {
271                        anyhow::bail!("not enough slice links");
272                    };
273
274                    match self.items.get(id as usize) {
275                        // Accept links to data slices.
276                        Some(Item::Data(DataBlock {
277                            data: Data::Slice(..),
278                        })) => {}
279                        // Accept links to inlined continuations.
280                        Some(Item::Code(code)) => {
281                            anyhow::ensure!(
282                                code.is_inline,
283                                "cell argument points to a non-inlined continuation"
284                            );
285                        }
286                        // Accept links to jump tables.
287                        Some(Item::JumpTable(..)) => {}
288                        // Reject other types.
289                        Some(_) => anyhow::bail!("invalid link type"),
290                        // Unknown link.
291                        None => anyhow::bail!("unknown link"),
292                    }
293
294                    OpcodeArg::Slice { id }
295                }
296            });
297        }
298
299        anyhow::ensure!(links.next().is_none(), "not all links were used");
300        Ok((name, res))
301    }
302
303    fn add_cont(&mut self, cont: Cell) -> Result<DumpStackItem, ItemId> {
304        if cont.descriptor().is_library() {
305            return Err(self.add_library(cont));
306        }
307
308        match self.cont_to_id.entry(*cont.repr_hash()) {
309            // First time we see this continuation.
310            hash_map::Entry::Vacant(entry) => {
311                // Insert a stub item to get the new id.
312                let id = add_item(&mut self.items, DataBlock {
313                    data: Cell::default().into(),
314                });
315                entry.insert(id);
316                // Create a stack item.
317                Ok(DumpStackItem::new(id, cont.into(), false, None))
318            }
319            // Continuation is already known.
320            hash_map::Entry::Occupied(entry) => Err(*entry.get()),
321        }
322    }
323
324    fn add_cont_slice(&mut self, cont: CellSliceParts) -> Result<DumpStackItem, ItemId> {
325        // First time we see this continuation.
326        match self.cont_slice_to_id.entry((*cont.1.repr_hash(), cont.0)) {
327            hash_map::Entry::Vacant(entry) => {
328                // Insert a stub item to get the new id.
329                let id = add_item(&mut self.items, DataBlock {
330                    data: CellSliceParts::from(Cell::default()).into(),
331                });
332                entry.insert(id);
333                // Create a stack item.
334                Ok(DumpStackItem::new(id, cont, true, None))
335            }
336            // Continuation is already known.
337            hash_map::Entry::Occupied(entry) => Err(*entry.get()),
338        }
339    }
340
341    fn add_jump_table(&mut self, n: u16, root: Cell) -> Result<NewJumpTable, ItemId> {
342        match self.dict_to_id.entry((n, *root.repr_hash())) {
343            // First time we see this jump table.
344            hash_map::Entry::Vacant(entry) => {
345                // Insert a new empty jump table.
346                let id = add_item(&mut self.items, JumpTable {
347                    cell_hash: *root.repr_hash(),
348                    key_bits: n,
349                    items: Vec::new(),
350                    is_full_code: true,
351                });
352                entry.insert(id);
353
354                // Parse jump table items into inline continuations.
355                let mut to_parse = Vec::new();
356                if (|| {
357                    for item in tycho_types::dict::RawOwnedIter::new(&Some(root), n) {
358                        let (key, value) = item?;
359                        let key = key.as_data_slice().load_bigint(n, true)?;
360
361                        let save_to = SaveTo {
362                            jump_table_id: id,
363                            key,
364                        };
365
366                        let id = add_item(&mut self.items, DataBlock {
367                            data: Cell::default().into(),
368                        });
369                        to_parse.push(DumpStackItem::new(id, value, true, Some(save_to)));
370                    }
371                    Ok::<_, tycho_types::error::Error>(())
372                })()
373                .is_ok()
374                {
375                    // Mark jump table as fully parsed if there were no errors.
376                    let Item::JumpTable(table) = &mut self.items[id as usize] else {
377                        unreachable!("table index mismatch");
378                    };
379                    table.is_full_code = true;
380                }
381
382                Ok(NewJumpTable { id, to_parse })
383            }
384            // Jump table is already known.
385            hash_map::Entry::Occupied(entry) => Err(*entry.get()),
386        }
387    }
388
389    fn add_cell(&mut self, cell: Cell) -> ItemId {
390        *self
391            .cell_to_id
392            .entry(*cell.repr_hash())
393            .or_insert_with(|| add_item(&mut self.items, DataBlock { data: cell.into() }))
394    }
395
396    fn add_slice(&mut self, slice: CellSliceParts) -> ItemId {
397        *self
398            .slice_to_id
399            .entry((*slice.1.repr_hash(), slice.0))
400            .or_insert_with(|| add_item(&mut self.items, DataBlock { data: slice.into() }))
401    }
402
403    fn add_library(&mut self, cell: Cell) -> ItemId {
404        debug_assert!(cell.descriptor().is_library());
405
406        let hash = *cell.repr_hash();
407        *self
408            .lib_to_id
409            .entry(hash)
410            .or_insert_with(|| add_item(&mut self.items, Library { cell_hash: hash }))
411    }
412}
413
414fn add_item<T: Into<Item>>(items: &mut Vec<Item>, item: T) -> ItemId {
415    let id = items.len() as ItemId;
416    items.push(item.into());
417    id
418}
419
420struct DumpStackItem {
421    id: ItemId,
422    block: CodeBlock,
423    slice: CellSliceParts,
424    save_to: Option<SaveTo>,
425}
426
427impl DumpStackItem {
428    fn new(id: ItemId, slice: CellSliceParts, is_inline: bool, save_to: Option<SaveTo>) -> Self {
429        Self {
430            id,
431            block: CodeBlock {
432                cell_hash: *slice.1.repr_hash(),
433                is_inline,
434                offset_bits: slice.0.offset_bits(),
435                offset_refs: slice.0.offset_refs(),
436                bits: slice.0.size_bits(),
437                refs: slice.0.size_refs(),
438                opcodes: Vec::new(),
439                tail: Some(CodeBlockTail::Incomplete),
440            },
441            slice,
442            save_to,
443        }
444    }
445}
446
447struct SaveTo {
448    jump_table_id: ItemId,
449    key: BigInt,
450}
451
452struct NewJumpTable {
453    id: ItemId,
454    to_parse: Vec<DumpStackItem>,
455}
456
457type SliceKey = (HashBytes, CellSliceRange);
458
459#[derive(Default)]
460struct DumpState {
461    root: Cell,
462    cells: Vec<Cell>,
463    slices: Vec<CellSliceParts>,
464    conts: Vec<Cell>,
465    cont_slices: Vec<CellSliceParts>,
466    dicts: Vec<(u16, Cell)>,
467    gas: u64,
468    opcode: String,
469}
470
471impl DumpState {
472    fn clear(&mut self) {
473        self.cells.clear();
474        self.slices.clear();
475        self.conts.clear();
476        self.cont_slices.clear();
477        self.dicts.clear();
478        self.gas = 0;
479        self.opcode.clear();
480    }
481}
482
483impl DumpOutput for DumpState {
484    fn record_gas(&mut self, gas: u64) -> DumpResult {
485        self.gas = gas;
486        Ok(())
487    }
488
489    fn record_opcode(&mut self, value: &dyn std::fmt::Display) -> DumpResult {
490        write!(&mut self.opcode, "{}", value)?;
491        Ok(())
492    }
493
494    fn record_cell(&mut self, value: Cell) -> DumpResult {
495        self.cells.push(value);
496        Ok(())
497    }
498
499    fn record_slice(&mut self, value: CellSlice<'_>) -> DumpResult {
500        if value.cell() != self.root.as_ref() {
501            return Err(DumpError::CellMismatch);
502        }
503        self.slices.push((value.range(), self.root.clone()));
504        Ok(())
505    }
506
507    fn record_cont(&mut self, cont: Cell) -> DumpResult {
508        self.conts.push(cont);
509        Ok(())
510    }
511
512    fn record_cont_slice(&mut self, cont: CellSlice<'_>) -> DumpResult {
513        if cont.cell() != self.root.as_ref() {
514            return Err(DumpError::CellMismatch);
515        }
516        self.cont_slices.push((cont.range(), self.root.clone()));
517        Ok(())
518    }
519
520    fn record_dict(&mut self, n: u16, mut slice: CellSlice<'_>) -> DumpResult {
521        self.dicts.push((n, slice.load_reference_cloned()?));
522        Ok(())
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use anyhow::Result;
529    use tycho_asm_macros::tvmasm;
530
531    use super::*;
532
533    #[test]
534    fn disasm_ever_wallet() -> Result<()> {
535        let code = Boc::decode_base64(
536            "te6ccgEBBgEA/AABFP8A9KQT9LzyyAsBAgEgBQIC5vJx1wEBwADyeoMI1xjtRNCDB9cB1ws/yPgozxYjzxbJ+QADcdcBAcMAmoMH1wFRE7ry4GTegEDXAYAg1wGAINcBVBZ1+RDyqPgju/J5Zr74I4EHCKCBA+ioUiC8sfJ0AiCCEEzuZGy64w8ByMv/yz/J7VQEAwA+ghAWnj4Ruo4R+AACkyDXSpd41wHUAvsA6NGTMvI84gCYMALXTND6QIMG1wFx1wF41wHXTPgAcIAQBKoCFLHIywVQBc8WUAP6AstpItAhzzEh10mghAm5mDNwAcsAWM8WlzBxAcsAEsziyQH7AAAE0jA=",
537        )?;
538        let code = disasm_structured(code)?;
539        println!("{}", serde_json::to_string_pretty(&code).unwrap());
540
541        // std::fs::write(
542        //     "ever_wallet.json",
543        //     serde_json::to_string_pretty(&code).unwrap(),
544        // )
545        // .unwrap();
546
547        Ok(())
548    }
549
550    #[test]
551    fn disasm_wallet_v3() -> Result<()> {
552        let code = Boc::decode_base64(
553            "te6ccgEBAQEAcQAA3v8AIN0gggFMl7ohggEznLqxn3Gw7UTQ0x/THzHXC//jBOCk8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVA==",
554        )?;
555        let code = disasm_structured(code)?;
556        println!("{}", serde_json::to_string_pretty(&code).unwrap());
557
558        // std::fs::write(
559        //     "wallet_v3.json",
560        //     serde_json::to_string_pretty(&code).unwrap(),
561        // )
562        // .unwrap();
563
564        Ok(())
565    }
566
567    #[test]
568    fn disasm_slice_data() -> Result<()> {
569        let code = Boc::decode(tvmasm!("PUSHSLICE x{6_}"))?;
570        let code = disasm_structured(code)?;
571        println!("{}", serde_json::to_string_pretty(&code).unwrap());
572
573        Ok(())
574    }
575
576    #[test]
577    fn disasm_cell_data() -> Result<()> {
578        let code = Boc::decode(tvmasm!("ACCEPT INT 123 PUSHREF x{6_}"))?;
579        println!("{}", Boc::encode_base64(&code));
580        let code = disasm_structured(code)?;
581        println!("{}", serde_json::to_string_pretty(&code).unwrap());
582
583        Ok(())
584    }
585
586    #[test]
587    fn disasm_elector() -> Result<()> {
588        let code = Boc::decode(include_bytes!("../res/elector_code.boc"))?;
589        let _code = disasm_structured(code)?;
590        // println!("{}", serde_json::to_string_pretty(&code).unwrap());
591
592        // std::fs::write("elector.json", serde_json::to_string_pretty(&code).unwrap()).unwrap();
593
594        Ok(())
595    }
596}