Skip to main content

endbasic_std/gpio/
mod.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! GPIO access functions and commands for EndBASIC.
18
19use endbasic_core::{
20    ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
21    CallableMetadataBuilder, ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
22};
23use std::any::Any;
24use std::borrow::Cow;
25use std::cell::RefCell;
26use std::io;
27use std::rc::Rc;
28
29mod fakes;
30pub use fakes::{MockPins, NoopPins};
31
32use crate::{Clearable, MachineBuilder};
33
34/// Category description for all symbols provided by this module.
35const CATEGORY: &str = "Hardware interface
36EndBASIC provides features to manipulate external hardware.  These features are currently limited \
37to GPIO interaction on a Raspberry Pi and are only available when EndBASIC has explicitly been \
38built with the --features=rpi option.  Support for other busses and platforms may come later.";
39
40/// Pin identifier.
41#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42pub struct Pin(pub u8);
43
44impl Pin {
45    /// Creates a new pin number from an EndBASIC integer value.
46    fn from_i32(i: i32) -> Result<Self, String> {
47        if i < 0 {
48            return Err(format!("Pin number {} must be positive", i));
49        }
50        if i > u8::MAX as i32 {
51            return Err(format!("Pin number {} is too large", i));
52        }
53        Ok(Self(i as u8))
54    }
55}
56
57/// Pin configuration, which includes mode and bias.
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum PinMode {
60    /// Pin that can be read from with no bias.
61    In,
62
63    /// Pin that can be read from with its built-in pull-down resistor (if present) enabled.
64    InPullDown,
65
66    /// Pin that can be read from with its built-in pull-up resistor (if present) enabled.
67    InPullUp,
68
69    /// Pin that can be written to.
70    Out,
71}
72
73impl PinMode {
74    /// Obtains a `PinMode` from a value.
75    fn parse(s: &str) -> Result<PinMode, String> {
76        match s.to_ascii_uppercase().as_ref() {
77            "IN" => Ok(PinMode::In),
78            "IN-PULL-UP" => Ok(PinMode::InPullUp),
79            "IN-PULL-DOWN" => Ok(PinMode::InPullDown),
80            "OUT" => Ok(PinMode::Out),
81            s => Err(format!("Unknown pin mode {}", s)),
82        }
83    }
84}
85
86fn parse_pin(scope: &Scope<'_>, narg: u8) -> CallResult<Pin> {
87    Pin::from_i32(scope.get_integer(narg)).map_err(|e| CallError::Syntax(scope.get_pos(narg), e))
88}
89
90fn parse_pin_mode(scope: &Scope<'_>, narg: u8) -> CallResult<PinMode> {
91    PinMode::parse(scope.get_string(narg)).map_err(|e| CallError::Syntax(scope.get_pos(narg), e))
92}
93
94/// Generic abstraction over a GPIO chip to back all EndBASIC commands.
95pub trait Pins {
96    /// Returns `self` as `&dyn Any` to allow downcasting to a concrete type.
97    fn as_any(&self) -> &dyn Any;
98
99    /// Returns `self` as `&mut dyn Any` to allow downcasting to a concrete type.
100    fn as_any_mut(&mut self) -> &mut dyn Any;
101
102    /// Configures the `pin` as either input or output (per `mode`).
103    ///
104    /// This lazily initialies the GPIO chip as well on the first pin setup.
105    ///
106    /// It is OK to set up a pin multiple times without calling `clear()` in-between.
107    fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()>;
108
109    /// Resets a given `pin` to its default state.
110    fn clear(&mut self, pin: Pin) -> io::Result<()>;
111
112    /// Resets all pins to their default state.
113    fn clear_all(&mut self) -> io::Result<()>;
114
115    /// Reads the value of the given `pin`, which must have been previously setup as an input pin.
116    fn read(&mut self, pin: Pin) -> io::Result<bool>;
117
118    /// Writes `v` to the given `pin`, which must have been previously setup as an output pin.
119    fn write(&mut self, pin: Pin, v: bool) -> io::Result<()>;
120}
121
122/// Resets the state of the pins in a best-effort manner.
123pub(crate) struct PinsClearable {
124    pins: Rc<RefCell<dyn Pins>>,
125}
126
127impl PinsClearable {
128    /// Creates a new clearable for `pins`.
129    pub(crate) fn new(pins: Rc<RefCell<dyn Pins>>) -> Box<Self> {
130        Box::from(Self { pins })
131    }
132}
133
134impl Clearable for PinsClearable {
135    fn reset_state(&self) {
136        let _ = self.pins.borrow_mut().clear_all();
137    }
138}
139
140/// The `GPIO_SETUP` command.
141pub struct GpioSetupCommand {
142    metadata: Rc<CallableMetadata>,
143    pins: Rc<RefCell<dyn Pins>>,
144}
145
146impl GpioSetupCommand {
147    /// Creates a new instance of the command.
148    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
149        Rc::from(Self {
150            metadata: CallableMetadataBuilder::new("GPIO_SETUP")
151                .with_syntax(&[(
152                    &[
153                        SingularArgSyntax::RequiredValue(
154                            RequiredValueSyntax {
155                                name: Cow::Borrowed("pin"),
156                                vtype: ExprType::Integer,
157                            },
158                            ArgSepSyntax::Exactly(ArgSep::Long),
159                        ),
160                        SingularArgSyntax::RequiredValue(
161                            RequiredValueSyntax {
162                                name: Cow::Borrowed("mode"),
163                                vtype: ExprType::Text,
164                            },
165                            ArgSepSyntax::End,
166                        ),
167                    ],
168                    None,
169                )])
170                .with_category(CATEGORY)
171                .with_description(
172                    "Configures a GPIO pin for input or output.
173Before a GPIO pin can be used for reads or writes, it must be configured to be an input or \
174output pin.  Additionally, if pull up or pull down resistors are available and desired, these \
175must be configured upfront too.
176The mode$ has to be one of \"IN\", \"IN-PULL-DOWN\", \"IN-PULL-UP\", or \"OUT\".  These values \
177are case-insensitive.  The possibility of using the pull-down and pull-up resistors depends on \
178whether they are available in the hardware, and selecting these modes will fail if they are not.
179It is OK to reconfigure an already configured pin without clearing its state first.",
180                )
181                .build(),
182            pins,
183        })
184    }
185}
186
187impl Callable for GpioSetupCommand {
188    fn metadata(&self) -> Rc<CallableMetadata> {
189        self.metadata.clone()
190    }
191
192    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
193        debug_assert_eq!(2, scope.nargs());
194        let pin = parse_pin(&scope, 0)?;
195        let mode = parse_pin_mode(&scope, 1)?;
196
197        self.pins.borrow_mut().setup(pin, mode).map_err(CallError::from)?;
198        Ok(())
199    }
200}
201
202/// The `GPIO_CLEAR` command.
203pub struct GpioClearCommand {
204    metadata: Rc<CallableMetadata>,
205    pins: Rc<RefCell<dyn Pins>>,
206}
207
208impl GpioClearCommand {
209    /// Creates a new instance of the command.
210    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
211        Rc::from(Self {
212            metadata: CallableMetadataBuilder::new("GPIO_CLEAR")
213                .with_syntax(&[
214                    (&[], None),
215                    (
216                        &[SingularArgSyntax::RequiredValue(
217                            RequiredValueSyntax {
218                                name: Cow::Borrowed("pin"),
219                                vtype: ExprType::Integer,
220                            },
221                            ArgSepSyntax::End,
222                        )],
223                        None,
224                    ),
225                ])
226                .with_category(CATEGORY)
227                .with_description(
228                    "Resets the GPIO chip or a specific pin.
229If no pin% is specified, resets the state of all GPIO pins. \
230If a pin% is given, only that pin is reset.  It is OK if the given pin has never been configured \
231before.",
232                )
233                .build(),
234            pins,
235        })
236    }
237}
238
239impl Callable for GpioClearCommand {
240    fn metadata(&self) -> Rc<CallableMetadata> {
241        self.metadata.clone()
242    }
243
244    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
245        if scope.nargs() == 0 {
246            self.pins.borrow_mut().clear_all().map_err(CallError::from)?;
247        } else {
248            debug_assert_eq!(1, scope.nargs());
249            let pin = parse_pin(&scope, 0)?;
250
251            self.pins.borrow_mut().clear(pin).map_err(CallError::from)?;
252        }
253
254        Ok(())
255    }
256}
257
258/// The `GPIO_READ` function.
259pub struct GpioReadFunction {
260    metadata: Rc<CallableMetadata>,
261    pins: Rc<RefCell<dyn Pins>>,
262}
263
264impl GpioReadFunction {
265    /// Creates a new instance of the function.
266    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
267        Rc::from(Self {
268            metadata: CallableMetadataBuilder::new("GPIO_READ")
269                .with_return_type(ExprType::Boolean)
270                .with_syntax(&[(
271                    &[SingularArgSyntax::RequiredValue(
272                        RequiredValueSyntax {
273                            name: Cow::Borrowed("pin"),
274                            vtype: ExprType::Integer,
275                        },
276                        ArgSepSyntax::End,
277                    )],
278                    None,
279                )])
280                .with_category(CATEGORY)
281                .with_description(
282                    "Reads the state of a GPIO pin.
283Returns FALSE to represent a low value, and TRUE to represent a high value.",
284                )
285                .build(),
286            pins,
287        })
288    }
289}
290
291impl Callable for GpioReadFunction {
292    fn metadata(&self) -> Rc<CallableMetadata> {
293        self.metadata.clone()
294    }
295
296    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
297        debug_assert_eq!(1, scope.nargs());
298        let pin = parse_pin(&scope, 0)?;
299
300        let value = self.pins.borrow_mut().read(pin).map_err(CallError::from)?;
301        scope.return_boolean(value)
302    }
303}
304
305/// The `GPIO_WRITE` command.
306pub struct GpioWriteCommand {
307    metadata: Rc<CallableMetadata>,
308    pins: Rc<RefCell<dyn Pins>>,
309}
310
311impl GpioWriteCommand {
312    /// Creates a new instance of the command.
313    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
314        Rc::from(Self {
315            metadata: CallableMetadataBuilder::new("GPIO_WRITE")
316                .with_syntax(&[(
317                    &[
318                        SingularArgSyntax::RequiredValue(
319                            RequiredValueSyntax {
320                                name: Cow::Borrowed("pin"),
321                                vtype: ExprType::Integer,
322                            },
323                            ArgSepSyntax::Exactly(ArgSep::Long),
324                        ),
325                        SingularArgSyntax::RequiredValue(
326                            RequiredValueSyntax {
327                                name: Cow::Borrowed("value"),
328                                vtype: ExprType::Boolean,
329                            },
330                            ArgSepSyntax::End,
331                        ),
332                    ],
333                    None,
334                )])
335                .with_category(CATEGORY)
336                .with_description(
337                    "Sets the state of a GPIO pin.
338A FALSE value? sets the pin to low, and a TRUE value? sets the pin to high.",
339                )
340                .build(),
341            pins,
342        })
343    }
344}
345
346impl Callable for GpioWriteCommand {
347    fn metadata(&self) -> Rc<CallableMetadata> {
348        self.metadata.clone()
349    }
350
351    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
352        debug_assert_eq!(2, scope.nargs());
353        let pin = parse_pin(&scope, 0)?;
354        let value = scope.get_boolean(1);
355
356        self.pins.borrow_mut().write(pin, value).map_err(CallError::from)?;
357        Ok(())
358    }
359}
360
361/// The `GPIO_MOCK_INJECT` command.
362pub struct GpioMockInjectCommand {
363    metadata: Rc<CallableMetadata>,
364    pins: Rc<RefCell<dyn Pins>>,
365}
366
367impl GpioMockInjectCommand {
368    /// Creates a new instance of the command.
369    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
370        Rc::from(Self {
371            metadata: CallableMetadataBuilder::new("GPIO_MOCK_INJECT")
372                .with_syntax(&[(
373                    &[
374                        SingularArgSyntax::RequiredValue(
375                            RequiredValueSyntax {
376                                name: Cow::Borrowed("pin"),
377                                vtype: ExprType::Integer,
378                            },
379                            ArgSepSyntax::Exactly(ArgSep::Long),
380                        ),
381                        SingularArgSyntax::RequiredValue(
382                            RequiredValueSyntax {
383                                name: Cow::Borrowed("high"),
384                                vtype: ExprType::Boolean,
385                            },
386                            ArgSepSyntax::End,
387                        ),
388                    ],
389                    None,
390                )])
391                .with_category(CATEGORY)
392                .with_description(
393                    "Pre-seeds a GPIO_READ result for testing.
394This command is only available when EndBASIC is started with --gpio-pins=mock.  It pre-seeds \
395the next GPIO_READ call for the given pin% to return the given high? value.",
396                )
397                .build(),
398            pins,
399        })
400    }
401}
402
403impl Callable for GpioMockInjectCommand {
404    fn metadata(&self) -> Rc<CallableMetadata> {
405        self.metadata.clone()
406    }
407
408    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
409        debug_assert_eq!(2, scope.nargs());
410        let pin = parse_pin(&scope, 0)?;
411        let high = scope.get_boolean(1);
412
413        self.pins
414            .borrow_mut()
415            .as_any_mut()
416            .downcast_mut::<MockPins>()
417            .expect("Only registered for mock backend")
418            .inject_read(pin, high);
419        Ok(())
420    }
421}
422
423/// The `GPIO_MOCK_TRACE` function.
424pub struct GpioMockTraceFunction {
425    metadata: Rc<CallableMetadata>,
426    pins: Rc<RefCell<dyn Pins>>,
427}
428
429impl GpioMockTraceFunction {
430    /// Creates a new instance of the function.
431    pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
432        Rc::from(Self {
433            metadata: CallableMetadataBuilder::new("GPIO_MOCK_TRACE")
434                .with_return_type(ExprType::Text)
435                .with_syntax(&[(&[], None)])
436                .with_category(CATEGORY)
437                .with_description(
438                    "Returns the GPIO operation trace for testing.
439This function is only available when EndBASIC is started with --gpio-pins=mock.  It returns a \
440space-separated list of integers representing the ordered record of all GPIO operations \
441performed since the last reset.",
442                )
443                .build(),
444            pins,
445        })
446    }
447}
448
449impl Callable for GpioMockTraceFunction {
450    fn metadata(&self) -> Rc<CallableMetadata> {
451        self.metadata.clone()
452    }
453
454    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
455        debug_assert_eq!(0, scope.nargs());
456        let pins = self.pins.borrow();
457        let mock =
458            pins.as_any().downcast_ref::<MockPins>().expect("Only registered for mock backend");
459        let result = mock.trace().iter().map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
460        scope.return_string(result)
461    }
462}
463
464/// Adds all symbols provided by this module to the given `machine`.
465pub fn add_all(machine: &mut MachineBuilder, pins: Rc<RefCell<dyn Pins>>) {
466    if pins.borrow().as_any().downcast_ref::<MockPins>().is_some() {
467        machine.add_callable(GpioMockInjectCommand::new(pins.clone()));
468        machine.add_callable(GpioMockTraceFunction::new(pins.clone()));
469    }
470
471    machine.add_clearable(PinsClearable::new(pins.clone()));
472    machine.add_callable(GpioClearCommand::new(pins.clone()));
473    machine.add_callable(GpioReadFunction::new(pins.clone()));
474    machine.add_callable(GpioSetupCommand::new(pins.clone()));
475    machine.add_callable(GpioWriteCommand::new(pins));
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::testutils::*;
482    use futures_lite::future::block_on;
483
484    /// Common checks for pin number validation.
485    ///
486    /// The given input `fmt` string contains the command to test with a placeholder `_PIN` for
487    /// where the pin number goes.  The `short_prefix` and `long_prefix` contain possible prefixes
488    /// for the error messages.
489    fn check_pin_validation(short_prefix: &str, long_prefix: &str, fmt: &str) {
490        check_stmt_compilation_err(
491            format!(r#"{}BOOLEAN is not a number"#, short_prefix),
492            &fmt.replace("_PIN_", "TRUE"),
493        );
494        check_stmt_err(
495            format!(r#"{}Pin number 123456789 is too large"#, long_prefix),
496            &fmt.replace("_PIN_", "123456789"),
497        );
498        check_stmt_err(
499            format!(r#"{}Pin number -1 must be positive"#, long_prefix),
500            &fmt.replace("_PIN_", "-1"),
501        );
502    }
503
504    /// Creates a machine backed by `MockPins` pre-seeded with `reads` and returns both the machine
505    /// and a handle to inspect the trace afterwards.
506    fn make_mock_machine(reads: &[(u8, bool)]) -> (crate::Machine, Rc<RefCell<MockPins>>) {
507        let mock_pins = Rc::new(RefCell::new(MockPins::default()));
508        for &(pin, high) in reads {
509            mock_pins.borrow_mut().inject_read(Pin(pin), high);
510        }
511        let pins: Rc<RefCell<dyn Pins>> = mock_pins.clone();
512        let machine = MachineBuilder::default().with_gpio_pins(pins).build();
513        (machine, mock_pins)
514    }
515
516    /// Runs `code` in a machine backed by MockPins pre-seeded with `reads` and asserts that the
517    /// resulting trace equals `expected_trace`.
518    fn do_mock_test(code: &str, reads: &[(u8, bool)], expected_trace: &[i32]) {
519        let (mut machine, mock_pins) = make_mock_machine(reads);
520        machine.compile(&mut code.as_bytes()).unwrap();
521        let _ = block_on(machine.exec()).unwrap();
522        assert_eq!(expected_trace, mock_pins.borrow().trace());
523    }
524
525    /// Tests that all GPIO operations delegate to the real pins implementation, which defaults to
526    /// the no-op backend when using the tester.  All other tests in this file use MockPins via
527    /// `make_mock_machine` to validate operation.
528    #[test]
529    fn test_real_backend() {
530        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_SETUP 0, \"IN\"");
531        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR");
532        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR 0");
533        check_expr_error("1:10: GPIO backend not compiled in", "GPIO_READ(0)");
534        check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_WRITE 0, TRUE");
535    }
536
537    #[test]
538    fn test_gpio_setup_ok() {
539        for mode in &["in", "IN"] {
540            do_mock_test(&format!(r#"GPIO_SETUP 5, "{}""#, mode), &[], &[501]);
541            do_mock_test(&format!(r#"GPIO_SETUP 5.2, "{}""#, mode), &[], &[501]);
542        }
543        for mode in &["in-pull-down", "IN-PULL-DOWN"] {
544            do_mock_test(&format!(r#"GPIO_SETUP 6, "{}""#, mode), &[], &[602]);
545            do_mock_test(&format!(r#"GPIO_SETUP 6.2, "{}""#, mode), &[], &[602]);
546        }
547        for mode in &["in-pull-up", "IN-PULL-UP"] {
548            do_mock_test(&format!(r#"GPIO_SETUP 7, "{}""#, mode), &[], &[703]);
549            do_mock_test(&format!(r#"GPIO_SETUP 7.2, "{}""#, mode), &[], &[703]);
550        }
551        for mode in &["out", "OUT"] {
552            do_mock_test(&format!(r#"GPIO_SETUP 8, "{}""#, mode), &[], &[804]);
553            do_mock_test(&format!(r#"GPIO_SETUP 8.2, "{}""#, mode), &[], &[804]);
554        }
555    }
556
557    #[test]
558    fn test_gpio_setup_multiple() {
559        do_mock_test(r#"GPIO_SETUP 18, "IN-PULL-UP": GPIO_SETUP 10, "OUT""#, &[], &[1803, 1004]);
560    }
561
562    #[test]
563    fn test_gpio_setup_errors() {
564        check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP"#);
565        check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1"#);
566        check_stmt_compilation_err("1:13: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1; 2"#);
567        check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1, 2, 3"#);
568
569        check_pin_validation("1:12: ", "1:12: ", r#"GPIO_SETUP _PIN_, "IN""#);
570
571        check_stmt_err(r#"1:15: Unknown pin mode IN-OUT"#, r#"GPIO_SETUP 1, "IN-OUT""#);
572    }
573
574    #[test]
575    fn test_gpio_clear_all() {
576        do_mock_test("GPIO_CLEAR", &[], &[-1]);
577    }
578
579    #[test]
580    fn test_gpio_clear_one() {
581        do_mock_test("GPIO_CLEAR 4", &[], &[405]);
582        do_mock_test("GPIO_CLEAR 4.1", &[], &[405]);
583    }
584
585    #[test]
586    fn test_gpio_clear_errors() {
587        check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | <pin%>", r#"GPIO_CLEAR 1,"#);
588        check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | <pin%>", r#"GPIO_CLEAR 1, 2"#);
589
590        check_pin_validation("1:12: ", "1:12: ", r#"GPIO_CLEAR _PIN_"#);
591    }
592
593    #[test]
594    fn test_gpio_read_ok() {
595        // Read pin 3 (low → GPIO_WRITE 5 low), then read pin 3 (high → GPIO_WRITE 7 high).
596        // GPIO_READ evaluates before GPIO_WRITE, so trace is: read3low, write5low, read3high, write7high.
597        do_mock_test(
598            "GPIO_WRITE 5, GPIO_READ(3.1): GPIO_WRITE 7, GPIO_READ(3)",
599            &[(3, false), (3, true)],
600            &[310, 520, 311, 721],
601        );
602    }
603
604    #[test]
605    fn test_gpio_read_errors() {
606        check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ()"#);
607        check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ(1, 2)"#);
608
609        check_pin_validation("1:15: ", "1:15: ", r#"v = GPIO_READ(_PIN_)"#);
610    }
611
612    #[test]
613    fn test_gpio_write_ok() {
614        do_mock_test("GPIO_WRITE 3, TRUE: GPIO_WRITE 3.1, FALSE", &[], &[321, 320]);
615    }
616
617    #[test]
618    fn test_gpio_write_errors() {
619        check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE"#);
620        check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 2,"#);
621        check_stmt_compilation_err(
622            "1:1: GPIO_WRITE expected pin%, value?",
623            r#"GPIO_WRITE 1, TRUE, 2"#,
624        );
625        check_stmt_compilation_err(
626            "1:13: GPIO_WRITE expected pin%, value?",
627            r#"GPIO_WRITE 1; TRUE"#,
628        );
629
630        check_pin_validation("1:12: ", "1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#);
631
632        check_stmt_compilation_err(
633            "1:15: Expected BOOLEAN but found INTEGER",
634            r#"GPIO_WRITE 1, 5"#,
635        );
636    }
637}