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;
const CATEGORY: &str = "Array functions";
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))
}
}
pub struct LboundFunction {
metadata: Rc<CallableMetadata>,
}
impl LboundFunction {
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)
}
}
pub struct UboundFunction {
metadata: Rc<CallableMetadata>,
}
impl UboundFunction {
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)
}
}
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::*;
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();
}
}