endbasic-std 0.13.0

The EndBASIC programming language - standard library
Documentation
// EndBASIC
// Copyright 2021 Julio Merino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! Array-related functions for EndBASIC.

use endbasic_core::{
    ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
    CallableMetadataBuilder, ExprType, RequiredRefSyntax, RequiredValueSyntax, Scope,
    SingularArgSyntax,
};
use std::borrow::Cow;
use std::rc::Rc;

use crate::MachineBuilder;

/// Category description for all symbols provided by this module.
const CATEGORY: &str = "Array functions";

/// Extracts array dimensions and the dimension number from args passed to `LBOUND` or `UBOUND`.
fn parse_bound_args(scope: &Scope<'_>) -> CallResult<(Vec<usize>, usize)> {
    let array = scope.get_ref(0);
    let dimensions = array.array_dimensions();

    if scope.nargs() == 2 {
        let i = scope.get_integer(1);

        if i < 0 {
            return Err(CallError::Syntax(
                scope.get_pos(1),
                format!("Dimension {} must be positive", i),
            ));
        }
        let i = i as usize;

        if i > dimensions.len() {
            return Err(CallError::Syntax(
                scope.get_pos(1),
                format!("Array has only {} dimensions but asked for {}", dimensions.len(), i,),
            ));
        }
        Ok((dimensions.to_vec(), i))
    } else {
        debug_assert_eq!(1, scope.nargs());

        if dimensions.len() > 1 {
            return Err(CallError::Syntax(
                scope.get_pos(0),
                "Requires a dimension for multidimensional arrays".to_owned(),
            ));
        }

        Ok((dimensions.to_vec(), 1))
    }
}

/// The `LBOUND` function.
pub struct LboundFunction {
    metadata: Rc<CallableMetadata>,
}

impl LboundFunction {
    /// Creates a new instance of the function.
    pub fn new() -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("LBOUND")
                .with_return_type(ExprType::Integer)
                .with_syntax(&[
                    (
                        &[SingularArgSyntax::RequiredRef(
                            RequiredRefSyntax {
                                name: Cow::Borrowed("array"),
                                require_array: true,
                                define_undefined: false,
                            },
                            ArgSepSyntax::End,
                        )],
                        None,
                    ),
                    (
                        &[
                            SingularArgSyntax::RequiredRef(
                                RequiredRefSyntax {
                                    name: Cow::Borrowed("array"),
                                    require_array: true,
                                    define_undefined: false,
                                },
                                ArgSepSyntax::Exactly(ArgSep::Long),
                            ),
                            SingularArgSyntax::RequiredValue(
                                RequiredValueSyntax {
                                    name: Cow::Borrowed("dimension"),
                                    vtype: ExprType::Integer,
                                },
                                ArgSepSyntax::End,
                            ),
                        ],
                        None,
                    ),
                ])
                .with_category(CATEGORY)
                .with_description(
                    "Returns the lower bound for the given dimension of the array.
The lower bound is the smallest available subscript that can be provided to array indexing \
operations.
For one-dimensional arrays, the dimension% is optional.  For multi-dimensional arrays, the \
dimension% is a 1-indexed integer.",
                )
                .build(),
        })
    }
}

impl Callable for LboundFunction {
    fn metadata(&self) -> Rc<CallableMetadata> {
        self.metadata.clone()
    }

    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
        let (_dimensions, _dim) = parse_bound_args(&scope)?;
        scope.return_integer(0)
    }
}

/// The `UBOUND` function.
pub struct UboundFunction {
    metadata: Rc<CallableMetadata>,
}

impl UboundFunction {
    /// Creates a new instance of the function.
    pub fn new() -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("UBOUND")
                .with_return_type(ExprType::Integer)
                .with_syntax(&[
                    (
                        &[SingularArgSyntax::RequiredRef(
                            RequiredRefSyntax {
                                name: Cow::Borrowed("array"),
                                require_array: true,
                                define_undefined: false,
                            },
                            ArgSepSyntax::End,
                        )],
                        None,
                    ),
                    (
                        &[
                            SingularArgSyntax::RequiredRef(
                                RequiredRefSyntax {
                                    name: Cow::Borrowed("array"),
                                    require_array: true,
                                    define_undefined: false,
                                },
                                ArgSepSyntax::Exactly(ArgSep::Long),
                            ),
                            SingularArgSyntax::RequiredValue(
                                RequiredValueSyntax {
                                    name: Cow::Borrowed("dimension"),
                                    vtype: ExprType::Integer,
                                },
                                ArgSepSyntax::End,
                            ),
                        ],
                        None,
                    ),
                ])
                .with_category(CATEGORY)
                .with_description(
                    "Returns the upper bound for the given dimension of the array.
The upper bound is the largest available subscript that can be provided to array indexing \
operations.
For one-dimensional arrays, the dimension% is optional.  For multi-dimensional arrays, the \
dimension% is a 1-indexed integer.",
                )
                .build(),
        })
    }
}

impl Callable for UboundFunction {
    fn metadata(&self) -> Rc<CallableMetadata> {
        self.metadata.clone()
    }

    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
        let (dimensions, dim) = parse_bound_args(&scope)?;
        scope.return_integer((dimensions[dim - 1] - 1) as i32)
    }
}

/// Adds all symbols provided by this module to the given `machine`.
pub fn add_all(machine: &mut MachineBuilder) {
    machine.add_callable(LboundFunction::new());
    machine.add_callable(UboundFunction::new());
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutils::*;

    /// Validates error handling of `LBOUND` and `UBOUND` as given in `func`.
    fn do_bound_errors_test(func: &str) {
        Tester::default()
            .run(format!("DIM x(2): result = {}()", func))
            .expect_compilation_err(format!(
                "1:20: {} expected <array> | <array, dimension%>",
                func
            ))
            .check();

        Tester::default()
            .run(format!("DIM x(2): result = {}(x, 1, 2)", func))
            .expect_compilation_err(format!(
                "1:20: {} expected <array> | <array, dimension%>",
                func
            ))
            .check();

        Tester::default()
            .run(format!("DIM x(2): result = {}(x, -1)", func))
            .expect_err("1:30: Dimension -1 must be positive")
            .expect_array("x", ExprType::Integer, &[2], vec![])
            .check();

        Tester::default()
            .run(format!("DIM x(2): result = {}(x, TRUE)", func))
            .expect_compilation_err("1:30: BOOLEAN is not a number")
            .check();

        Tester::default()
            .run(format!("i = 0: result = {}(i)", func))
            .expect_compilation_err(format!(
                "1:24: {} expected <array> | <array, dimension%>",
                func
            ))
            .check();

        Tester::default()
            .run(format!("result = {}(3)", func))
            .expect_compilation_err(format!(
                "1:17: {} expected <array> | <array, dimension%>",
                func
            ))
            .check();

        Tester::default()
            .run(format!("i = 0: result = {}(i)", func))
            .expect_compilation_err(format!(
                "1:24: {} expected <array> | <array, dimension%>",
                func
            ))
            .check();

        Tester::default()
            .run(format!("DIM i(3) AS BOOLEAN: result = {}(i$)", func))
            .expect_compilation_err("1:38: Incompatible type annotation in i$ reference")
            .check();

        Tester::default()
            .run(format!("result = {}(x)", func))
            .expect_compilation_err("1:17: Undefined symbol x")
            .check();

        Tester::default()
            .run(format!("DIM x(2, 3, 4): result = {}(x)", func))
            .expect_err("1:33: Requires a dimension for multidimensional arrays")
            .expect_array("x", ExprType::Integer, &[2, 3, 4], vec![])
            .check();

        Tester::default()
            .run(format!("DIM x(2, 3, 4): result = {}(x, 5)", func))
            .expect_err("1:36: Array has only 3 dimensions but asked for 5")
            .expect_array("x", ExprType::Integer, &[2, 3, 4], vec![])
            .check();
    }

    #[test]
    fn test_lbound_ok() {
        Tester::default()
            .run("DIM x(10): result = LBOUND(x)")
            .expect_var("result", 0i32)
            .expect_array("x", ExprType::Integer, &[10], vec![])
            .check();

        Tester::default()
            .run("DIM x(10, 20): result = LBOUND(x, 1)")
            .expect_var("result", 0i32)
            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
            .check();

        Tester::default()
            .run("DIM x(10, 20): result = LBOUND(x, 2.1)")
            .expect_var("result", 0i32)
            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
            .check();
    }

    #[test]
    fn test_lbound_errors() {
        do_bound_errors_test("LBOUND");
    }

    #[test]
    fn test_ubound_ok() {
        Tester::default()
            .run("DIM x(10): result = UBOUND(x)")
            .expect_var("result", 9i32)
            .expect_array("x", ExprType::Integer, &[10], vec![])
            .check();

        Tester::default()
            .run("DIM x(10, 20): result = UBOUND(x, 1)")
            .expect_var("result", 9i32)
            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
            .check();

        Tester::default()
            .run("DIM x(10, 20): result = UBOUND(x, 2.1)")
            .expect_var("result", 19i32)
            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
            .check();
    }

    #[test]
    fn test_ubound_errors() {
        do_bound_errors_test("UBOUND");
    }

    #[test]
    fn test_bound_integration() {
        Tester::default()
            .run("DIM x(5): FOR i = LBOUND(x) TO UBOUND(x): x(i) = i * 2: NEXT")
            .expect_var("i", 5i32)
            .expect_array_simple(
                "x",
                ExprType::Integer,
                vec![0i32.into(), 2i32.into(), 4i32.into(), 6i32.into(), 8i32.into()],
            )
            .check();
    }
}