iota_transactional_test_runner/programmable_transaction_test_parser/
parser.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{borrow::BorrowMut, marker::PhantomData, str::FromStr};
6
7use anyhow::{Context, Result, bail};
8use iota_types::{
9    base_types::ObjectID,
10    transaction::{Argument, Command, ProgrammableMoveCall},
11};
12use move_core_types::{
13    account_address::AccountAddress,
14    identifier::Identifier,
15    parsing::{
16        parser::{Parser, Token},
17        types::{ParsedType, TypeToken},
18    },
19};
20
21use super::token::CommandToken;
22use crate::programmable_transaction_test_parser::token::{
23    GAS_COIN, INPUT, MAKE_MOVE_VEC, MERGE_COINS, NESTED_RESULT, PUBLISH, RESULT, SPLIT_COINS,
24    TRANSFER_OBJECTS, UPGRADE,
25};
26
27/// A small parser used for parsing programmable transaction commands for
28/// transactional tests
29pub struct CommandParser<
30    'a,
31    I: Iterator<Item = (CommandToken, &'a str)>,
32    P: BorrowMut<Parser<'a, CommandToken, I>>,
33> {
34    inner: P,
35    _a: PhantomData<&'a ()>,
36    _i: PhantomData<I>,
37}
38
39#[derive(Debug, Clone)]
40pub struct ParsedMoveCall {
41    pub package: Identifier,
42    pub module: Identifier,
43    pub function: Identifier,
44    pub type_arguments: Vec<ParsedType>,
45    pub arguments: Vec<Argument>,
46}
47
48#[derive(Debug, Clone)]
49pub enum ParsedCommand {
50    MoveCall(Box<ParsedMoveCall>),
51    TransferObjects(Vec<Argument>, Argument),
52    SplitCoins(Argument, Vec<Argument>),
53    MergeCoins(Argument, Vec<Argument>),
54    MakeMoveVec(Option<ParsedType>, Vec<Argument>),
55    Publish(String, Vec<String>),
56    Upgrade(String, Vec<String>, String, Argument),
57}
58
59impl<'a, I: Iterator<Item = (CommandToken, &'a str)>>
60    CommandParser<'a, I, Parser<'a, CommandToken, I>>
61{
62    pub fn new<T: IntoIterator<Item = (CommandToken, &'a str), IntoIter = I>>(v: T) -> Self {
63        Self::from_parser(Parser::new(v))
64    }
65}
66
67impl<'a, I, P> CommandParser<'a, I, P>
68where
69    I: Iterator<Item = (CommandToken, &'a str)>,
70    P: BorrowMut<Parser<'a, CommandToken, I>>,
71{
72    pub fn from_parser(inner: P) -> Self {
73        Self {
74            inner,
75            _a: PhantomData,
76            _i: PhantomData,
77        }
78    }
79
80    pub fn parse_commands(&mut self) -> Result<Vec<ParsedCommand>> {
81        let commands = self.inner().parse_list(
82            |p| CommandParser::from_parser(p).parse_command_start(),
83            CommandToken::Semi,
84            // not checked
85            CommandToken::Void,
86            // allow_trailing_delim
87            true,
88        )?;
89        let commands = commands
90            .into_iter()
91            .enumerate()
92            .map(|(actual, (annotated, c))| {
93                if let Some(annotated) = annotated {
94                    if actual != annotated {
95                        anyhow::bail!(
96                            "Actual command index of {actual} \
97                            does not match annotated index {annotated}",
98                        );
99                    }
100                }
101                Ok(c)
102            })
103            .collect::<Result<_>>()?;
104        Ok(commands)
105    }
106
107    pub fn parse_command_start(&mut self) -> Result<(Option<usize>, ParsedCommand)> {
108        self.inner().advance(CommandToken::CommandStart)?;
109        let idx = if let Some(CommandToken::Number) = self.inner().peek_tok() {
110            let num = self.inner().advance(CommandToken::Number)?;
111            let idx = usize::from_str(num).context("Invalid command index annotation")?;
112            self.inner().advance(CommandToken::Colon)?;
113            Some(idx)
114        } else {
115            None
116        };
117        let cmd = self.parse_command()?;
118        Ok((idx, cmd))
119    }
120
121    pub fn parse_command(&mut self) -> Result<ParsedCommand> {
122        use super::token::CommandToken as Tok;
123        Ok(match self.inner().advance_any()? {
124            (Tok::Ident, TRANSFER_OBJECTS) => {
125                self.inner().advance(Tok::LParen)?;
126                let args = self.parse_command_args(Tok::LBracket, Tok::RBracket)?;
127                self.inner().advance(Tok::Comma)?;
128                let arg = self.parse_command_arg()?;
129                self.maybe_trailing_comma()?;
130                self.inner().advance(Tok::RParen)?;
131                ParsedCommand::TransferObjects(args, arg)
132            }
133            (Tok::Ident, SPLIT_COINS) => {
134                self.inner().advance(Tok::LParen)?;
135                let coin = self.parse_command_arg()?;
136                self.inner().advance(Tok::Comma)?;
137                let amts = self.parse_command_args(Tok::LBracket, Tok::RBracket)?;
138                self.maybe_trailing_comma()?;
139                self.inner().advance(Tok::RParen)?;
140                ParsedCommand::SplitCoins(coin, amts)
141            }
142            (Tok::Ident, MERGE_COINS) => {
143                self.inner().advance(Tok::LParen)?;
144                let target = self.parse_command_arg()?;
145                self.inner().advance(Tok::Comma)?;
146                let coins = self.parse_command_args(Tok::LBracket, Tok::RBracket)?;
147                self.maybe_trailing_comma()?;
148                self.inner().advance(Tok::RParen)?;
149                ParsedCommand::MergeCoins(target, coins)
150            }
151            (Tok::Ident, MAKE_MOVE_VEC) => {
152                let type_opt = self.parse_type_arg_opt()?;
153                self.inner().advance(Tok::LParen)?;
154                let args = self.parse_command_args(Tok::LBracket, Tok::RBracket)?;
155                self.maybe_trailing_comma()?;
156                self.inner().advance(Tok::RParen)?;
157                ParsedCommand::MakeMoveVec(type_opt, args)
158            }
159            (Tok::Ident, PUBLISH) => {
160                self.inner().advance(Tok::LParen)?;
161                let staged_package = self.inner().advance(Tok::Ident)?;
162                self.inner().advance(Tok::Comma)?;
163                self.inner().advance(Tok::LBracket)?;
164                let dependencies = self.inner().parse_list(
165                    |p| Ok(p.advance(Tok::Ident)?.to_owned()),
166                    CommandToken::Comma,
167                    Tok::RBracket,
168                    // allow_trailing_delim
169                    true,
170                )?;
171                self.inner().advance(Tok::RBracket)?;
172                self.maybe_trailing_comma()?;
173                self.inner().advance(Tok::RParen)?;
174                ParsedCommand::Publish(staged_package.to_owned(), dependencies)
175            }
176            (Tok::Ident, UPGRADE) => {
177                self.inner().advance(Tok::LParen)?;
178                let staged_package = self.inner().advance(Tok::Ident)?;
179                self.inner().advance(Tok::Comma)?;
180                self.inner().advance(Tok::LBracket)?;
181                let dependencies = self.inner().parse_list(
182                    |p| Ok(p.advance(Tok::Ident)?.to_owned()),
183                    CommandToken::Comma,
184                    Tok::RBracket,
185                    // allow_trailing_delim
186                    true,
187                )?;
188                self.inner().advance(Tok::RBracket)?;
189                self.inner().advance(Tok::Comma)?;
190                let upgraded_package = self.inner().advance(Tok::Ident)?;
191                self.inner().advance(Tok::Comma)?;
192                let upgrade_ticket = self.parse_command_arg()?;
193                self.maybe_trailing_comma()?;
194                self.inner().advance(Tok::RParen)?;
195                ParsedCommand::Upgrade(
196                    staged_package.to_owned(),
197                    dependencies,
198                    upgraded_package.to_owned(),
199                    upgrade_ticket,
200                )
201            }
202            (Tok::Ident, contents) => {
203                let package = Identifier::new(contents)?;
204                self.inner().advance(Tok::ColonColon)?;
205                let module = Identifier::new(self.inner().advance(Tok::Ident)?)?;
206                self.inner().advance(Tok::ColonColon)?;
207                let function = Identifier::new(self.inner().advance(Tok::Ident)?)?;
208                let type_arguments = self.parse_type_args_opt()?.unwrap_or_default();
209                let arguments = self.parse_command_args(Tok::LParen, Tok::RParen)?;
210                let call = ParsedMoveCall {
211                    package,
212                    module,
213                    function,
214                    type_arguments,
215                    arguments,
216                };
217                ParsedCommand::MoveCall(Box::new(call))
218            }
219
220            (tok, _) => bail!("unexpected token {}, expected command identifier", tok),
221        })
222    }
223
224    pub fn maybe_trailing_comma(&mut self) -> Result<()> {
225        if let Some(CommandToken::Comma) = self.inner().peek_tok() {
226            self.inner().advance(CommandToken::Comma)?;
227        }
228        Ok(())
229    }
230
231    pub fn parse_command_args(
232        &mut self,
233        start: CommandToken,
234        end: CommandToken,
235    ) -> Result<Vec<Argument>> {
236        self.inner().advance(start)?;
237        let args = self.inner().parse_list(
238            |p| CommandParser::from_parser(p).parse_command_arg(),
239            CommandToken::Comma,
240            end,
241            // allow_trailing_delim
242            true,
243        )?;
244        self.inner().advance(end)?;
245        Ok(args)
246    }
247
248    pub fn parse_command_arg(&mut self) -> Result<Argument> {
249        use super::token::CommandToken as Tok;
250        Ok(match self.inner().advance_any()? {
251            (Tok::Ident, GAS_COIN) => Argument::GasCoin,
252            (Tok::Ident, INPUT) => {
253                self.inner().advance(Tok::LParen)?;
254                let num = self.parse_u16()?;
255                self.maybe_trailing_comma()?;
256                self.inner().advance(Tok::RParen)?;
257                Argument::Input(num)
258            }
259            (Tok::Ident, RESULT) => {
260                self.inner().advance(Tok::LParen)?;
261                let num = self.parse_u16()?;
262                self.maybe_trailing_comma()?;
263                self.inner().advance(Tok::RParen)?;
264                Argument::Result(num)
265            }
266            (Tok::Ident, NESTED_RESULT) => {
267                self.inner().advance(Tok::LParen)?;
268                let i = self.parse_u16()?;
269                self.inner().advance(Tok::Comma)?;
270                let j = self.parse_u16()?;
271                self.maybe_trailing_comma()?;
272                self.inner().advance(Tok::RParen)?;
273                Argument::NestedResult(i, j)
274            }
275            (tok, _) => bail!("unexpected token {}, expected argument identifier", tok),
276        })
277    }
278
279    pub fn parse_u16(&mut self) -> Result<u16> {
280        let contents = self.inner().advance(CommandToken::Number)?;
281        u16::from_str(contents).context("Expected u16 for Argument")
282    }
283
284    pub fn parse_type_arg_opt(&mut self) -> Result<Option<ParsedType>> {
285        match self.parse_type_args_opt()? {
286            None => Ok(None),
287            Some(v) if v.len() != 1 => bail!(
288                "unexpected multiple type arguments. Expected 1 type argument but got {}",
289                v.len()
290            ),
291            Some(mut v) => Ok(Some(v.pop().unwrap())),
292        }
293    }
294
295    pub fn parse_type_args_opt(&mut self) -> Result<Option<Vec<ParsedType>>> {
296        if !matches!(self.inner().peek_tok(), Some(CommandToken::TypeArgString)) {
297            return Ok(None);
298        }
299        let contents = self.inner().advance(CommandToken::TypeArgString)?;
300        let type_tokens: Vec<_> = TypeToken::tokenize(contents)?
301            .into_iter()
302            .filter(|(tok, _)| !tok.is_whitespace())
303            .collect();
304        let mut parser = Parser::new(type_tokens);
305        parser.advance(TypeToken::Lt)?;
306        let res = parser.parse_list(|p| p.parse_type(), TypeToken::Comma, TypeToken::Gt, true)?;
307        parser.advance(TypeToken::Gt)?;
308        if let Ok((_, contents)) = parser.advance_any() {
309            bail!("Expected end of token stream. Got: {}", contents)
310        }
311        Ok(Some(res))
312    }
313
314    pub fn inner(&mut self) -> &mut Parser<'a, CommandToken, I> {
315        self.inner.borrow_mut()
316    }
317}
318
319impl ParsedCommand {
320    pub fn parse_vec(s: &str) -> Result<Vec<Self>> {
321        let tokens: Vec<_> = CommandToken::tokenize(s)?
322            .into_iter()
323            .filter(|(tok, _)| !tok.is_whitespace())
324            .collect();
325        let mut parser = CommandParser::new(tokens);
326        let res = parser.parse_commands()?;
327        if let Ok((_, contents)) = parser.inner().advance_any() {
328            bail!("Expected end of token stream. Got: {}", contents)
329        }
330        Ok(res)
331    }
332
333    pub fn into_command(
334        self,
335        staged_packages: &impl Fn(&str) -> Option<Vec<Vec<u8>>>,
336        address_mapping: &impl Fn(&str) -> Option<AccountAddress>,
337    ) -> Result<Command> {
338        Ok(match self {
339            ParsedCommand::MoveCall(c) => {
340                Command::MoveCall(Box::new(c.into_move_call(address_mapping)?))
341            }
342            ParsedCommand::TransferObjects(objs, recipient) => {
343                Command::TransferObjects(objs, recipient)
344            }
345            ParsedCommand::SplitCoins(coin, amts) => Command::SplitCoins(coin, amts),
346            ParsedCommand::MergeCoins(target, coins) => Command::MergeCoins(target, coins),
347            ParsedCommand::MakeMoveVec(ty_opt, args) => Command::MakeMoveVec(
348                ty_opt
349                    .map(|t| t.into_type_tag(address_mapping))
350                    .transpose()?,
351                args,
352            ),
353            ParsedCommand::Publish(staged_package, dependencies) => {
354                let Some(package_contents) = staged_packages(&staged_package) else {
355                    bail!("No staged package '{staged_package}'");
356                };
357                let dependencies = dependencies
358                    .into_iter()
359                    .map(|d| match address_mapping(&d) {
360                        Some(a) => Ok(a.into()),
361                        None => bail!("Unbound dependency '{d}"),
362                    })
363                    .collect::<Result<Vec<ObjectID>>>()?;
364                Command::Publish(package_contents, dependencies)
365            }
366            ParsedCommand::Upgrade(staged_package, dependencies, upgraded_package, ticket) => {
367                let Some(package_contents) = staged_packages(&staged_package) else {
368                    bail!("No staged package '{staged_package}'");
369                };
370                let dependencies = dependencies
371                    .into_iter()
372                    .map(|d| match address_mapping(&d) {
373                        Some(a) => Ok(a.into()),
374                        None => bail!("Unbound dependency '{d}"),
375                    })
376                    .collect::<Result<Vec<ObjectID>>>()?;
377                let Some(upgraded_package) = address_mapping(&upgraded_package) else {
378                    bail!("Unbound upgraded package '{upgraded_package}'");
379                };
380                let upgraded_package = upgraded_package.into();
381                Command::Upgrade(package_contents, dependencies, upgraded_package, ticket)
382            }
383        })
384    }
385}
386
387impl ParsedMoveCall {
388    pub fn into_move_call(
389        self,
390        address_mapping: &impl Fn(&str) -> Option<AccountAddress>,
391    ) -> Result<ProgrammableMoveCall> {
392        let Self {
393            package,
394            module,
395            function,
396            type_arguments,
397            arguments,
398        } = self;
399        let Some(package) = address_mapping(package.as_str()) else {
400            bail!("Unable to resolve package {}", package)
401        };
402        let type_arguments = type_arguments
403            .into_iter()
404            .map(|t| t.into_type_tag(address_mapping))
405            .collect::<Result<_>>()?;
406        Ok(ProgrammableMoveCall {
407            package: package.into(),
408            module,
409            function,
410            type_arguments,
411            arguments,
412        })
413    }
414}