Skip to main content

cmsis_pdsc_parser/
debug_access.rs

1//! Types representing  [PDSC Debug Access](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_family_pg.html#block_DebugSyntaxRules)
2
3use serde::{Deserialize, Serialize};
4
5use crate::debug_access::Statement::Comment;
6
7/// Parse error for debug access XML elements.
8#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
9pub enum DebugAccessParseError {
10    /// A required attribute or structural element was absent.
11    MissingAttribute(String),
12    /// An unrecognised statement or function name was encountered.
13    UnknownStatement(String),
14}
15
16impl Default for DebugAccessParseError {
17    fn default() -> Self {
18        Self::UnknownStatement(String::default())
19    }
20}
21
22impl From<DebugAccessParseError> for crate::Error {
23    fn from(value: DebugAccessParseError) -> Self {
24        Self::Debug(value)
25    }
26}
27
28impl std::fmt::Display for DebugAccessParseError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::MissingAttribute(msg) => write!(f, "missing attribute: {msg}"),
32            Self::UnknownStatement(name) => write!(f, "unknown statement: {name}"),
33        }
34    }
35}
36
37impl std::error::Error for DebugAccessParseError {}
38
39#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
40/// Types representing the valid [PDSC Debug Access](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_family_pg.html#block_DebugSyntaxRules)
41/// statements.
42pub enum Statement {
43    /// A sole expression, e.g. `expression;`
44    Expression(Expression),
45
46    /// A variable assignment, e.g. `variable = expression;`
47    Assignment(Assignment),
48
49    /// A variable definition, e.g. `__var variable = 0;`
50    Definition(Assignment),
51
52    /// Comment, e.g. `// This is a comment`
53    Comment(String),
54}
55
56impl Default for Statement {
57    fn default() -> Self {
58        Comment(String::default())
59    }
60}
61
62impl TryFrom<String> for Statement {
63    type Error = crate::Error;
64
65    fn try_from(value: String) -> Result<Self, Self::Error> {
66        // If present trim any whitespace
67        let input = value.trim().to_string();
68
69        // Check if it is a comment
70        if input.starts_with("//") {
71            return Ok(Self::Comment(input));
72        }
73
74        // If present remove the semicolon
75        let input = input.strip_suffix(";").unwrap_or(&input).to_string();
76
77        // Check if this is an assignment or declaration
78        let split: Option<(&str, &str)> = input.split_once('=');
79        let result: Self = match split {
80            None => {
81                // No '=', must be a standalone expression
82                let expression: Expression = input.try_into()?;
83                Self::Expression(expression)
84            }
85            Some((variable, expression)) => {
86                let variable = variable.trim();
87                let expression = expression.trim();
88                variable.strip_prefix("__var").map_or_else(
89                    || {
90                        let expression: Expression = expression.try_into()?;
91                        Ok::<Self, Self::Error>(Self::Assignment(Assignment {
92                            variable: variable.to_string(),
93                            expression,
94                        }))
95                    },
96                    |variable| {
97                        let variable = variable.trim();
98                        let expression: Expression = expression.try_into()?;
99                        Ok::<Self, Self::Error>(Self::Definition(Assignment {
100                            variable: variable.to_string(),
101                            expression,
102                        }))
103                    },
104                )?
105            }
106        };
107
108        Ok(result)
109    }
110}
111
112#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
113/// A variable assignment, e.g. `variable = expression;`
114pub struct Assignment {
115    pub variable: String,
116    pub expression: Expression,
117}
118
119#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
120/// A variable representing a [PDSC Expression](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/pdsc_family_pg.html#block_ExpressionType)
121pub enum Expression {
122    /// An arithmetic, bitwise, or comparison expression, e.g. `2 + 2`, `reg & 0xFF`, `x == 1`, or a bare variable reference
123    Normal(String),
124
125    /// An expression representing an inline if statement, e.g. `(x < y) ? a : b`
126    ///
127    /// # Note
128    ///
129    /// The parser currently does not handle nested conditionals, e.g. `(x < y) ? ( (a < b) ? c : d ) : e`
130    /// I hope noone has written a PDSC file which does this, if so this can be implemented.
131    Conditional(Box<Conditional>),
132
133    /// A call to a predefined [PDSC debug access function](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/debug_description.html#DebugFunctions),
134    /// e.g. `Read32(0x40000000)` or `Sequence("ResetAndHalt")`
135    FunctionCall(Box<DebugFunction>),
136}
137
138impl Default for Expression {
139    fn default() -> Self {
140        Self::Normal(String::default())
141    }
142}
143
144impl TryFrom<String> for Expression {
145    type Error = crate::Error;
146
147    fn try_from(value: String) -> Result<Self, Self::Error> {
148        Ok(Self::try_from(value.as_str())?)
149    }
150}
151
152impl TryFrom<&str> for Expression {
153    type Error = DebugAccessParseError;
154
155    fn try_from(value: &str) -> Result<Self, Self::Error> {
156        if let Ok(condition) = Conditional::try_from(value) {
157            return Ok(Self::Conditional(Box::new(condition)));
158        }
159
160        if let Some((name, args_str)) = detect_function_call(value) {
161            let args: Vec<Self> = split_args(args_str)
162                .into_iter()
163                .map(Self::try_from)
164                .collect::<Result<Vec<_>, _>>()?;
165            let func = DebugFunction::try_from((name.to_string(), args))?;
166
167            return Ok(Self::FunctionCall(Box::new(func)));
168        }
169
170        Ok(Self::Normal(value.to_string()))
171    }
172}
173
174#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
175/// An expression representing an inline if statement, e.g. `(x < y) ? a : b`
176///
177/// # Note
178///
179/// The parser currently does not handle nested conditionals, e.g. `(x < y) ? ( (a < b) ? c : d ) : e`
180/// I hope noone has written a PDSC file which does this, if so this can be implemented.
181pub struct Conditional {
182    /// The conditional part, `(x < y) ? a : b -> x < y`
183    pub condition: Expression,
184    /// The value when the conditional evaluates to true, `(x < y) ? a : b -> a`
185    pub true_value: Expression,
186    /// The value when the conditional evaluates to false, `(x < y) ? a : b -> b`
187    pub false_value: Expression,
188}
189
190impl TryFrom<String> for Conditional {
191    type Error = DebugAccessParseError;
192
193    /// Performs the conversion between [String] and [Conditional]
194    ///
195    /// # Note
196    ///
197    /// The parser currently does not handle nested conditionals, e.g. `(x < y) ? ( (a < b) ? c : d ) : e`
198    /// I hope noone has written a PDSC file which does this, if so this can be implemented.
199    /// This will return a valid type with a garbage value.
200    fn try_from(value: String) -> Result<Self, Self::Error> {
201        Self::try_from(value.as_str())
202    }
203}
204
205impl TryFrom<&str> for Conditional {
206    type Error = DebugAccessParseError;
207
208    /// Performs the conversion between [&str] and [Conditional]
209    ///
210    /// # Note
211    ///
212    /// The parser currently does not handle nested conditionals, e.g. `(x < y) ? ( (a < b) ? c : d ) : e`
213    /// I hope noone has written a PDSC file which does this, if so this can be implemented.
214    /// This will return a valid type with a garbage value.
215    fn try_from(value: &str) -> Result<Self, Self::Error> {
216        // Create the sates for the state machine
217        #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
218        enum WalkerProgress {
219            #[default]
220            None,
221            ParenOpen,
222            ParenClose,
223            Question,
224            Colon,
225        }
226
227        // Variables to store the result
228        let mut condition_str: String = String::new();
229        let mut truthy_str: String = String::new();
230        let mut falsey_str: String = String::new();
231
232        // Use a state machine to walk the string
233        let mut progress = WalkerProgress::None;
234        for c in value.chars() {
235            match progress {
236                WalkerProgress::None => {
237                    if c == '(' {
238                        progress = WalkerProgress::ParenOpen;
239                    }
240                }
241                WalkerProgress::ParenOpen => {
242                    if c == ')' {
243                        progress = WalkerProgress::ParenClose;
244                    } else {
245                        condition_str.push(c);
246                    }
247                }
248                WalkerProgress::ParenClose => {
249                    if c == '?' {
250                        progress = WalkerProgress::Question;
251                    }
252                }
253                WalkerProgress::Question => {
254                    if c == ':' {
255                        progress = WalkerProgress::Colon;
256                    } else {
257                        truthy_str.push(c);
258                    }
259                }
260                WalkerProgress::Colon => {
261                    if c == ';' {
262                        break;
263                    }
264                    falsey_str.push(c);
265                }
266            }
267        }
268
269        let walk_ok = progress == WalkerProgress::Colon && !falsey_str.is_empty();
270
271        if walk_ok {
272            let condition: Expression = condition_str.trim().try_into()?;
273            let true_value: Expression = truthy_str.trim().try_into()?;
274            let false_value: Expression = falsey_str.trim().try_into()?;
275
276            Ok(Self {
277                condition,
278                true_value,
279                false_value,
280            })
281        } else {
282            Err(DebugAccessParseError::MissingAttribute(
283                "conditional syntax: expected '(condition) ? truthy : falsy'".to_string(),
284            ))
285        }
286    }
287}
288
289#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
290/// A predefined [PDSC debug access function](https://open-cmsis-pack.github.io/Open-CMSIS-Pack-Spec/main/html/debug_description.html#DebugFunctions).
291///
292/// Unknown function names are a parse error — if the spec adds new functions they will surface as panics.
293pub enum DebugFunction {
294    // Memory access
295    /// Read 8-bit value from target memory
296    Read8 { addr: Expression },
297    /// Read 16-bit value from target memory
298    Read16 { addr: Expression },
299    /// Read 32-bit value from target memory
300    Read32 { addr: Expression },
301    /// Read 64-bit value from target memory
302    Read64 { addr: Expression },
303    /// Write 8-bit value to target memory
304    Write8 { addr: Expression, val: Expression },
305    /// Write 16-bit value to target memory
306    Write16 { addr: Expression, val: Expression },
307    /// Write 32-bit value to target memory
308    Write32 { addr: Expression, val: Expression },
309    /// Write 64-bit value to target memory
310    Write64 { addr: Expression, val: Expression },
311
312    // Register access
313    /// Read access port register
314    ReadAP { addr: Expression },
315    /// Write access port register
316    WriteAP { addr: Expression, val: Expression },
317    /// Read debug port register
318    ReadDP { addr: Expression },
319    /// Write debug port register
320    WriteDP { addr: Expression, val: Expression },
321    /// APv2/ADIv6 access port read
322    ReadAccessAP { addr: Expression },
323    /// APv2/ADIv6 access port write
324    WriteAccessAP { addr: Expression, val: Expression },
325
326    // Debug port / probe
327    /// Wait for a specific delay (microseconds)
328    DapDelay { delay: Expression },
329    /// Write abort request to CoreSight register
330    DapWriteAbort { value: Expression },
331    /// Monitor and control debugger I/O pins
332    DapSwjPins {
333        pinout: Expression,
334        pinselect: Expression,
335        pinwait: Expression,
336    },
337    /// Set JTAG/SWD clock frequency (Hz)
338    DapSwjClock { val: Expression },
339    /// Generate SWJ sequences
340    DapSwjSequence { cnt: Expression, val: Expression },
341    /// Generate JTAG sequences
342    DapJtagSequence {
343        cnt: Expression,
344        tms: Expression,
345        tdi: Expression,
346    },
347
348    // Sequence control
349    /// Execute a debug access sequence by name
350    Sequence { name: Expression },
351    /// Prompt user for confirmation or selection
352    Query {
353        query_type: Expression,
354        message: Expression,
355        default: Expression,
356    },
357    /// Query an input value from the user
358    QueryValue {
359        message: Expression,
360        default: Expression,
361    },
362    /// Output a formatted message to the debug log (variadic: `msg_type`, `format`, then optional extra args)
363    Message {
364        msg_type: Expression,
365        format: Expression,
366        args: Vec<Expression>,
367    },
368
369    // Flash operations
370    /// Write flash buffer contents into target memory
371    FlashWriteBuffer {
372        addr: Expression,
373        offs: Expression,
374        len: Expression,
375        mode: Expression,
376    },
377    /// Select FLM flash algorithm for operations
378    FlashLoadAlgorithm {
379        algo_path: Expression,
380        ram_start: Expression,
381        ram_size: Expression,
382    },
383
384    // Buffer management
385    /// Fill buffer with a value pattern
386    BufferSet {
387        buff_id: Expression,
388        buff_offset: Expression,
389        count: Expression,
390        size: Expression,
391        value: Expression,
392    },
393    /// Retrieve an item from a buffer
394    BufferGet {
395        buff_id: Expression,
396        buff_offset: Expression,
397        size: Expression,
398    },
399    /// Get current buffer size in bytes
400    BufferSize { buff_id: Expression },
401    /// Read target data into a buffer
402    BufferRead {
403        buff_id: Expression,
404        buff_offset: Expression,
405        addr: Expression,
406        length: Expression,
407        mode: Expression,
408    },
409    /// Transfer buffer data to target
410    BufferWrite {
411        buff_id: Expression,
412        buff_offset: Expression,
413        addr: Expression,
414        length: Expression,
415        mode: Expression,
416    },
417
418    // External tool integration
419    /// Stream data from an external source into a buffer
420    BufferStreamIn {
421        buff_id: Expression,
422        buff_offset: Expression,
423        length: Expression,
424        path: Expression,
425        mode: Expression,
426        timeout: Expression,
427    },
428    /// Transfer buffer data to an external sink
429    BufferStreamOut {
430        buff_id: Expression,
431        buff_offset: Expression,
432        length: Expression,
433        dest_path: Expression,
434        dest_mode: Expression,
435        timeout: Expression,
436    },
437    /// Execute an external application
438    RunApplication {
439        app_path: Expression,
440        arguments: Expression,
441        work_directory: Expression,
442        timeout: Expression,
443    },
444    /// Run a Python script on the host system
445    RunPythonScript {
446        script_path: Expression,
447        arguments: Expression,
448        work_directory: Expression,
449        timeout: Expression,
450    },
451    /// Check if a path exists on the host filesystem
452    FilePathExists {
453        path: Expression,
454        timeout: Expression,
455    },
456    /// Load DWARF debug information
457    LoadDebugInfo { file: Expression },
458}
459
460impl Default for DebugFunction {
461    fn default() -> Self {
462        Self::DapDelay {
463            delay: Expression::Normal("0".to_string()),
464        }
465    }
466}
467
468impl TryFrom<(String, Vec<Expression>)> for DebugFunction {
469    type Error = DebugAccessParseError;
470
471    /// Parses a debug access function by name and argument list.
472    ///
473    /// Returns [Err] if the function name is not in the CMSIS-Pack spec or the argument count is wrong.
474    #[allow(clippy::too_many_lines)]
475    fn try_from((name, args): (String, Vec<Expression>)) -> Result<Self, Self::Error> {
476        match name.as_str() {
477            // Memory — 1 arg (addr)
478            "Read8" => match <[Expression; 1]>::try_from(args) {
479                Ok([addr]) => Ok(Self::Read8 { addr }),
480                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
481                    "Read8 expects 1 argument, got {}",
482                    v.len()
483                ))),
484            },
485            "Read16" => match <[Expression; 1]>::try_from(args) {
486                Ok([addr]) => Ok(Self::Read16 { addr }),
487                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
488                    "Read16 expects 1 argument, got {}",
489                    v.len()
490                ))),
491            },
492            "Read32" => match <[Expression; 1]>::try_from(args) {
493                Ok([addr]) => Ok(Self::Read32 { addr }),
494                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
495                    "Read32 expects 1 argument, got {}",
496                    v.len()
497                ))),
498            },
499            "Read64" => match <[Expression; 1]>::try_from(args) {
500                Ok([addr]) => Ok(Self::Read64 { addr }),
501                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
502                    "Read64 expects 1 argument, got {}",
503                    v.len()
504                ))),
505            },
506            // Memory — 2 args (addr, val)
507            "Write8" => match <[Expression; 2]>::try_from(args) {
508                Ok([addr, val]) => Ok(Self::Write8 { addr, val }),
509                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
510                    "Write8 expects 2 arguments, got {}",
511                    v.len()
512                ))),
513            },
514            "Write16" => match <[Expression; 2]>::try_from(args) {
515                Ok([addr, val]) => Ok(Self::Write16 { addr, val }),
516                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
517                    "Write16 expects 2 arguments, got {}",
518                    v.len()
519                ))),
520            },
521            "Write32" => match <[Expression; 2]>::try_from(args) {
522                Ok([addr, val]) => Ok(Self::Write32 { addr, val }),
523                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
524                    "Write32 expects 2 arguments, got {}",
525                    v.len()
526                ))),
527            },
528            "Write64" => match <[Expression; 2]>::try_from(args) {
529                Ok([addr, val]) => Ok(Self::Write64 { addr, val }),
530                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
531                    "Write64 expects 2 arguments, got {}",
532                    v.len()
533                ))),
534            },
535            // Register — 1 arg (addr)
536            "ReadAP" => match <[Expression; 1]>::try_from(args) {
537                Ok([addr]) => Ok(Self::ReadAP { addr }),
538                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
539                    "ReadAP expects 1 argument, got {}",
540                    v.len()
541                ))),
542            },
543            "ReadDP" => match <[Expression; 1]>::try_from(args) {
544                Ok([addr]) => Ok(Self::ReadDP { addr }),
545                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
546                    "ReadDP expects 1 argument, got {}",
547                    v.len()
548                ))),
549            },
550            "ReadAccessAP" => match <[Expression; 1]>::try_from(args) {
551                Ok([addr]) => Ok(Self::ReadAccessAP { addr }),
552                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
553                    "ReadAccessAP expects 1 argument, got {}",
554                    v.len()
555                ))),
556            },
557            // Register — 2 args (addr, val)
558            "WriteAP" => match <[Expression; 2]>::try_from(args) {
559                Ok([addr, val]) => Ok(Self::WriteAP { addr, val }),
560                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
561                    "WriteAP expects 2 arguments, got {}",
562                    v.len()
563                ))),
564            },
565            "WriteDP" => match <[Expression; 2]>::try_from(args) {
566                Ok([addr, val]) => Ok(Self::WriteDP { addr, val }),
567                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
568                    "WriteDP expects 2 arguments, got {}",
569                    v.len()
570                ))),
571            },
572            "WriteAccessAP" => match <[Expression; 2]>::try_from(args) {
573                Ok([addr, val]) => Ok(Self::WriteAccessAP { addr, val }),
574                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
575                    "WriteAccessAP expects 2 arguments, got {}",
576                    v.len()
577                ))),
578            },
579            // Debug port — 1 arg
580            "DAP_Delay" => match <[Expression; 1]>::try_from(args) {
581                Ok([delay]) => Ok(Self::DapDelay { delay }),
582                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
583                    "DAP_Delay expects 1 argument, got {}",
584                    v.len()
585                ))),
586            },
587            "DAP_WriteABORT" => match <[Expression; 1]>::try_from(args) {
588                Ok([value]) => Ok(Self::DapWriteAbort { value }),
589                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
590                    "DAP_WriteABORT expects 1 argument, got {}",
591                    v.len()
592                ))),
593            },
594            "DAP_SWJ_Clock" => match <[Expression; 1]>::try_from(args) {
595                Ok([val]) => Ok(Self::DapSwjClock { val }),
596                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
597                    "DAP_SWJ_Clock expects 1 argument, got {}",
598                    v.len()
599                ))),
600            },
601            // Debug port — 2 args
602            "DAP_SWJ_Sequence" => match <[Expression; 2]>::try_from(args) {
603                Ok([cnt, val]) => Ok(Self::DapSwjSequence { cnt, val }),
604                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
605                    "DAP_SWJ_Sequence expects 2 arguments, got {}",
606                    v.len()
607                ))),
608            },
609            // Debug port — 3 args
610            "DAP_SWJ_Pins" => match <[Expression; 3]>::try_from(args) {
611                Ok([pinout, pinselect, pinwait]) => Ok(Self::DapSwjPins {
612                    pinout,
613                    pinselect,
614                    pinwait,
615                }),
616                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
617                    "DAP_SWJ_Pins expects 3 arguments, got {}",
618                    v.len()
619                ))),
620            },
621            "DAP_JTAG_Sequence" => match <[Expression; 3]>::try_from(args) {
622                Ok([cnt, tms, tdi]) => Ok(Self::DapJtagSequence { cnt, tms, tdi }),
623                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
624                    "DAP_JTAG_Sequence expects 3 arguments, got {}",
625                    v.len()
626                ))),
627            },
628            // Sequence control — 1 arg
629            "Sequence" => match <[Expression; 1]>::try_from(args) {
630                Ok([name]) => Ok(Self::Sequence { name }),
631                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
632                    "Sequence expects 1 argument, got {}",
633                    v.len()
634                ))),
635            },
636            // Sequence control — 2 args
637            "QueryValue" => match <[Expression; 2]>::try_from(args) {
638                Ok([message, default]) => Ok(Self::QueryValue { message, default }),
639                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
640                    "QueryValue expects 2 arguments, got {}",
641                    v.len()
642                ))),
643            },
644            // Sequence control — 3 args
645            "Query" => match <[Expression; 3]>::try_from(args) {
646                Ok([query_type, message, default]) => Ok(Self::Query {
647                    query_type,
648                    message,
649                    default,
650                }),
651                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
652                    "Query expects 3 arguments, got {}",
653                    v.len()
654                ))),
655            },
656            // Sequence control — variadic (2+ args)
657            "Message" => {
658                let mut it = args.into_iter();
659                let msg_type = it.next().ok_or_else(|| {
660                    DebugAccessParseError::MissingAttribute(
661                        "Message expects at least 2 arguments, got 0".to_string(),
662                    )
663                })?;
664                let format_expr = it.next().ok_or_else(|| {
665                    DebugAccessParseError::MissingAttribute(
666                        "Message expects at least 2 arguments, got 1".to_string(),
667                    )
668                })?;
669                Ok(Self::Message {
670                    msg_type,
671                    format: format_expr,
672                    args: it.collect(),
673                })
674            }
675            // Flash — 3 args
676            "FlashLoadAlgorithm" => match <[Expression; 3]>::try_from(args) {
677                Ok([algo_path, ram_start, ram_size]) => Ok(Self::FlashLoadAlgorithm {
678                    algo_path,
679                    ram_start,
680                    ram_size,
681                }),
682                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
683                    "FlashLoadAlgorithm expects 3 arguments, got {}",
684                    v.len()
685                ))),
686            },
687            // Flash — 4 args
688            "FlashWriteBuffer" => match <[Expression; 4]>::try_from(args) {
689                Ok([addr, offs, len, mode]) => Ok(Self::FlashWriteBuffer {
690                    addr,
691                    offs,
692                    len,
693                    mode,
694                }),
695                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
696                    "FlashWriteBuffer expects 4 arguments, got {}",
697                    v.len()
698                ))),
699            },
700            // Buffer — 1 arg
701            "BufferSize" => match <[Expression; 1]>::try_from(args) {
702                Ok([buff_id]) => Ok(Self::BufferSize { buff_id }),
703                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
704                    "BufferSize expects 1 argument, got {}",
705                    v.len()
706                ))),
707            },
708            // Buffer — 3 args
709            "BufferGet" => match <[Expression; 3]>::try_from(args) {
710                Ok([buff_id, buff_offset, size]) => Ok(Self::BufferGet {
711                    buff_id,
712                    buff_offset,
713                    size,
714                }),
715                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
716                    "BufferGet expects 3 arguments, got {}",
717                    v.len()
718                ))),
719            },
720            // Buffer — 5 args
721            "BufferSet" => match <[Expression; 5]>::try_from(args) {
722                Ok([buff_id, buff_offset, count, size, value]) => Ok(Self::BufferSet {
723                    buff_id,
724                    buff_offset,
725                    count,
726                    size,
727                    value,
728                }),
729                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
730                    "BufferSet expects 5 arguments, got {}",
731                    v.len()
732                ))),
733            },
734            "BufferRead" => match <[Expression; 5]>::try_from(args) {
735                Ok([buff_id, buff_offset, addr, length, mode]) => Ok(Self::BufferRead {
736                    buff_id,
737                    buff_offset,
738                    addr,
739                    length,
740                    mode,
741                }),
742                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
743                    "BufferRead expects 5 arguments, got {}",
744                    v.len()
745                ))),
746            },
747            "BufferWrite" => match <[Expression; 5]>::try_from(args) {
748                Ok([buff_id, buff_offset, addr, length, mode]) => Ok(Self::BufferWrite {
749                    buff_id,
750                    buff_offset,
751                    addr,
752                    length,
753                    mode,
754                }),
755                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
756                    "BufferWrite expects 5 arguments, got {}",
757                    v.len()
758                ))),
759            },
760            // External — 1 arg
761            "LoadDebugInfo" => match <[Expression; 1]>::try_from(args) {
762                Ok([file]) => Ok(Self::LoadDebugInfo { file }),
763                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
764                    "LoadDebugInfo expects 1 argument, got {}",
765                    v.len()
766                ))),
767            },
768            // External — 2 args
769            "FilePathExists" => match <[Expression; 2]>::try_from(args) {
770                Ok([path, timeout]) => Ok(Self::FilePathExists { path, timeout }),
771                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
772                    "FilePathExists expects 2 arguments, got {}",
773                    v.len()
774                ))),
775            },
776            // External — 4 args
777            "RunApplication" => match <[Expression; 4]>::try_from(args) {
778                Ok([app_path, arguments, work_directory, timeout]) => Ok(Self::RunApplication {
779                    app_path,
780                    arguments,
781                    work_directory,
782                    timeout,
783                }),
784                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
785                    "RunApplication expects 4 arguments, got {}",
786                    v.len()
787                ))),
788            },
789            "RunPythonScript" => match <[Expression; 4]>::try_from(args) {
790                Ok([script_path, arguments, work_directory, timeout]) => {
791                    Ok(Self::RunPythonScript {
792                        script_path,
793                        arguments,
794                        work_directory,
795                        timeout,
796                    })
797                }
798                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
799                    "RunPythonScript expects 4 arguments, got {}",
800                    v.len()
801                ))),
802            },
803            // External — 6 args
804            "BufferStreamIn" => match <[Expression; 6]>::try_from(args) {
805                Ok([buff_id, buff_offset, length, path, mode, timeout]) => {
806                    Ok(Self::BufferStreamIn {
807                        buff_id,
808                        buff_offset,
809                        length,
810                        path,
811                        mode,
812                        timeout,
813                    })
814                }
815                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
816                    "BufferStreamIn expects 6 arguments, got {}",
817                    v.len()
818                ))),
819            },
820            "BufferStreamOut" => match <[Expression; 6]>::try_from(args) {
821                Ok([buff_id, buff_offset, length, dest_path, dest_mode, timeout]) => {
822                    Ok(Self::BufferStreamOut {
823                        buff_id,
824                        buff_offset,
825                        length,
826                        dest_path,
827                        dest_mode,
828                        timeout,
829                    })
830                }
831                Err(v) => Err(DebugAccessParseError::MissingAttribute(format!(
832                    "BufferStreamOut expects 6 arguments, got {}",
833                    v.len()
834                ))),
835            },
836            _ => Err(DebugAccessParseError::UnknownStatement(name)),
837        }
838    }
839}
840
841/// Returns `Some((name, args_str))` if `s` matches `identifier(...)`, otherwise `None`.
842///
843/// `name` is the function name; `args_str` is the raw content between the outer parentheses.
844fn detect_function_call(s: &str) -> Option<(&str, &str)> {
845    if !s.ends_with(')') {
846        return None;
847    }
848
849    let paren_pos = s.find('(')?;
850    #[allow(clippy::string_slice)]
851    // Safety:
852    //   This is known to have a lot of false positives, and is OK if
853    //   given a valid position, which `find` should return.
854    let name = &s[..paren_pos];
855
856    // Validate name is a non-empty identifier [A-Za-z_][A-Za-z0-9_]*
857    let mut name_chars = name.chars();
858    let first = name_chars.next()?;
859    if !first.is_alphabetic() && first != '_' {
860        return None;
861    }
862    if !name_chars.all(|c| c.is_alphanumeric() || c == '_') {
863        return None;
864    }
865
866    #[allow(clippy::arithmetic_side_effects)]
867    // Safety:
868    //   While in theory `paren_pos + 1` could overflow it is
869    //   extremely unlikely, if so `s.len()` would also have
870    //   problems.
871    #[allow(clippy::string_slice)]
872    // Safety:
873    //   This is known to have a lot of false positives, and is OK if
874    //   given a valid position, which `find` should return. The end
875    //   of the string should also always be a valid position.
876    let args_str = &s[paren_pos + 1..s.len() - 1];
877    Some((name, args_str))
878}
879
880/// Splits a comma-separated argument string into trimmed segments, respecting nested parentheses.
881///
882/// e.g. `"addr, Read32(base)"` → `["addr", "Read32(base)"]`
883fn split_args(args_str: &str) -> Vec<&str> {
884    if args_str.trim().is_empty() {
885        return Vec::new();
886    }
887
888    let mut result = Vec::new();
889    let mut depth: u32 = 0u32;
890    let mut start: usize = 0;
891
892    #[allow(clippy::arithmetic_side_effects)]
893    // Safety:
894    //   If you have nested to `u32::MAX` I will be thoroughly impressed
895    for (i, c) in args_str.char_indices() {
896        match c {
897            '(' => depth += 1,
898            ')' => depth -= 1,
899            ',' if depth == 0 => {
900                #[allow(clippy::string_slice)]
901                // Safety:
902                //   We are iterating over char indices which is
903                //   explicitly used as a false positive in the clippy
904                //   documentation.
905                result.push(args_str[start..i].trim());
906                start = i + 1;
907            }
908            _ => {}
909        }
910    }
911
912    #[allow(clippy::string_slice)]
913    // Safety:
914    //   `start` value was obtained via `char_indices`
915    let last = args_str[start..].trim();
916    if !last.is_empty() {
917        result.push(last);
918    }
919
920    result
921}
922
923#[cfg(test)]
924mod tests {
925    use crate::debug_access::{
926        Assignment, Conditional, DebugAccessParseError, DebugFunction, Expression, Statement,
927    };
928
929    #[test]
930    fn parse_comment() {
931        let line = "// This is a comment!".to_string();
932
933        let statement: Statement = line.try_into().unwrap();
934
935        assert_eq!(
936            statement,
937            Statement::Comment("// This is a comment!".to_string())
938        );
939    }
940
941    #[test]
942    fn semicolon_handling() {
943        let line1 = "Read32(0x10)".to_string();
944        let line2 = "Read32(0x10);".to_string();
945
946        let statement1: Statement = line1.try_into().unwrap();
947        let statement2: Statement = line2.try_into().unwrap();
948
949        assert_eq!(statement1, statement2);
950    }
951
952    #[test]
953    fn parse_expression_normal() {
954        let line = "addr + offset;".to_string();
955
956        let statement: Statement = line.try_into().unwrap();
957
958        assert_eq!(
959            statement,
960            Statement::Expression(Expression::Normal("addr + offset".to_string()))
961        );
962    }
963
964    #[test]
965    fn parse_expression_normal_variable() {
966        let line = "doIfBlock".to_string();
967
968        let statement: Statement = line.try_into().unwrap();
969
970        assert_eq!(
971            statement,
972            Statement::Expression(Expression::Normal("doIfBlock".to_string()))
973        );
974    }
975
976    #[test]
977    fn parse_expression_conditional() {
978        let line = "(x < y) ? a : b".to_string();
979
980        let statement: Statement = line.try_into().unwrap();
981
982        assert_eq!(
983            statement,
984            Statement::Expression(Expression::Conditional(Box::new(Conditional {
985                condition: Expression::Normal("x < y".to_string()),
986                true_value: Expression::Normal("a".to_string()),
987                false_value: Expression::Normal("b".to_string())
988            })))
989        );
990    }
991
992    #[test]
993    fn parse_assignment_comparison() {
994        let line = "thisValue = (readTheCoolRegister(0x248) == 5);".to_string();
995
996        let statement: Statement = line.try_into().unwrap();
997
998        assert_eq!(
999            statement,
1000            Statement::Assignment(Assignment {
1001                variable: "thisValue".to_string(),
1002                expression: Expression::Normal("(readTheCoolRegister(0x248) == 5)".to_string())
1003            })
1004        );
1005    }
1006
1007    #[test]
1008    fn parse_assignment() {
1009        let line = "variable = expression;".to_string();
1010
1011        let statement: Statement = line.try_into().unwrap();
1012
1013        assert_eq!(
1014            statement,
1015            Statement::Assignment(Assignment {
1016                expression: Expression::Normal("expression".to_string()),
1017                variable: "variable".to_string(),
1018            })
1019        )
1020    }
1021
1022    #[test]
1023    fn parse_definition() {
1024        let line = "__var variable = 0;".to_string();
1025
1026        let statement: Statement = line.try_into().unwrap();
1027
1028        assert_eq!(
1029            statement,
1030            Statement::Definition(Assignment {
1031                expression: Expression::Normal("0".to_string()),
1032                variable: "variable".to_string(),
1033            })
1034        )
1035    }
1036
1037    #[test]
1038    fn parse_function_call_single_arg() {
1039        let line = "Read32(0x40000000);".to_string();
1040
1041        let statement: Statement = line.try_into().unwrap();
1042
1043        assert_eq!(
1044            statement,
1045            Statement::Expression(Expression::FunctionCall(Box::new(DebugFunction::Read32 {
1046                addr: Expression::Normal("0x40000000".to_string())
1047            })))
1048        );
1049    }
1050
1051    #[test]
1052    fn parse_function_call_two_args() {
1053        let line = "Write32(addr, val);".to_string();
1054
1055        let statement: Statement = line.try_into().unwrap();
1056
1057        assert_eq!(
1058            statement,
1059            Statement::Expression(Expression::FunctionCall(Box::new(DebugFunction::Write32 {
1060                addr: Expression::Normal("addr".to_string()),
1061                val: Expression::Normal("val".to_string()),
1062            })))
1063        );
1064    }
1065
1066    #[test]
1067    fn parse_function_call_string_arg() {
1068        let line = "Sequence(\"ResetAndHalt\");".to_string();
1069
1070        let statement: Statement = line.try_into().unwrap();
1071
1072        assert_eq!(
1073            statement,
1074            Statement::Expression(Expression::FunctionCall(Box::new(
1075                DebugFunction::Sequence {
1076                    name: Expression::Normal("\"ResetAndHalt\"".to_string())
1077                }
1078            )))
1079        );
1080    }
1081
1082    #[test]
1083    fn parse_function_call_three_args() {
1084        let line = "DAP_SWJ_Pins(pinout, pinselect, pinwait);".to_string();
1085
1086        let statement: Statement = line.try_into().unwrap();
1087
1088        assert_eq!(
1089            statement,
1090            Statement::Expression(Expression::FunctionCall(Box::new(
1091                DebugFunction::DapSwjPins {
1092                    pinout: Expression::Normal("pinout".to_string()),
1093                    pinselect: Expression::Normal("pinselect".to_string()),
1094                    pinwait: Expression::Normal("pinwait".to_string()),
1095                }
1096            )))
1097        );
1098    }
1099
1100    #[test]
1101    fn parse_function_call_variadic() {
1102        let line = "Message(1, \"debug message\");".to_string();
1103
1104        let statement: Statement = line.try_into().unwrap();
1105
1106        assert_eq!(
1107            statement,
1108            Statement::Expression(Expression::FunctionCall(Box::new(DebugFunction::Message {
1109                msg_type: Expression::Normal("1".to_string()),
1110                format: Expression::Normal("\"debug message\"".to_string()),
1111                args: vec![],
1112            })))
1113        );
1114    }
1115
1116    #[test]
1117    fn parse_function_call_nested_arg() {
1118        // Read32(base) is an argument to Write32 — split_args must not split on the inner comma
1119        let line = "Write32(addr, Read32(base));".to_string();
1120
1121        let statement: Statement = line.try_into().unwrap();
1122
1123        assert_eq!(
1124            statement,
1125            Statement::Expression(Expression::FunctionCall(Box::new(DebugFunction::Write32 {
1126                addr: Expression::Normal("addr".to_string()),
1127                val: Expression::FunctionCall(Box::new(DebugFunction::Read32 {
1128                    addr: Expression::Normal("base".to_string()),
1129                })),
1130            })))
1131        );
1132    }
1133
1134    #[test]
1135    #[should_panic(expected = "unknown statement: GetBase")]
1136    fn unknown_function_panics() {
1137        if let Err(e) = Expression::try_from("GetBase()") {
1138            panic!("{e}");
1139        }
1140    }
1141
1142    #[test]
1143    fn conditional_missing_syntax() {
1144        let result = Conditional::try_from("no parentheses here");
1145        assert!(matches!(
1146            result,
1147            Err(DebugAccessParseError::MissingAttribute(_))
1148        ));
1149    }
1150
1151    #[test]
1152    fn unknown_function_returns_unknown_statement() {
1153        let result = DebugFunction::try_from(("GetBase".to_string(), vec![]));
1154        assert_eq!(
1155            result.unwrap_err(),
1156            DebugAccessParseError::UnknownStatement("GetBase".to_string())
1157        );
1158    }
1159}