endbasic_std/
arrays.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Array-related functions for EndBASIC.
17
18use async_trait::async_trait;
19use endbasic_core::ast::{ArgSep, ExprType, VarRef};
20use endbasic_core::compiler::{
21    ArgSepSyntax, RequiredRefSyntax, RequiredValueSyntax, SingularArgSyntax,
22};
23use endbasic_core::exec::{Machine, Scope};
24use endbasic_core::syms::{
25    Array, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbol,
26    Symbols,
27};
28use std::borrow::Cow;
29use std::rc::Rc;
30
31/// Category description for all symbols provided by this module.
32const CATEGORY: &str = "Array functions";
33
34/// Extracts the array reference and the dimension number from the list of arguments passed to
35/// either `LBOUND` or `UBOUND`.
36#[allow(clippy::needless_lifetimes)]
37fn parse_bound_args<'a>(
38    scope: &mut Scope<'_>,
39    symbols: &'a Symbols,
40) -> Result<(&'a Array, usize), CallError> {
41    let (arrayname, arraytype, arraypos) = scope.pop_varref_with_pos();
42
43    let arrayref = VarRef::new(arrayname.to_string(), Some(arraytype));
44    let array = match symbols
45        .get(&arrayref)
46        .map_err(|e| CallError::ArgumentError(arraypos, format!("{}", e)))?
47    {
48        Some(Symbol::Array(array)) => array,
49        _ => unreachable!(),
50    };
51
52    if scope.nargs() == 1 {
53        let (i, pos) = scope.pop_integer_with_pos();
54
55        if i < 0 {
56            return Err(CallError::ArgumentError(pos, format!("Dimension {} must be positive", i)));
57        }
58        let i = i as usize;
59
60        if i > array.dimensions().len() {
61            return Err(CallError::ArgumentError(
62                pos,
63                format!(
64                    "Array {} has only {} dimensions but asked for {}",
65                    arrayname,
66                    array.dimensions().len(),
67                    i,
68                ),
69            ));
70        }
71        Ok((array, i))
72    } else {
73        debug_assert_eq!(0, scope.nargs());
74
75        if array.dimensions().len() > 1 {
76            return Err(CallError::ArgumentError(
77                arraypos,
78                "Requires a dimension for multidimensional arrays".to_owned(),
79            ));
80        }
81
82        Ok((array, 1))
83    }
84}
85
86/// The `LBOUND` function.
87pub struct LboundFunction {
88    metadata: CallableMetadata,
89}
90
91impl LboundFunction {
92    /// Creates a new instance of the function.
93    pub fn new() -> Rc<Self> {
94        Rc::from(Self {
95            metadata: CallableMetadataBuilder::new("LBOUND")
96                .with_return_type(ExprType::Integer)
97                .with_syntax(&[
98                    (
99                        &[SingularArgSyntax::RequiredRef(
100                            RequiredRefSyntax {
101                                name: Cow::Borrowed("array"),
102                                require_array: true,
103                                define_undefined: false,
104                            },
105                            ArgSepSyntax::End,
106                        )],
107                        None,
108                    ),
109                    (
110                        &[
111                            SingularArgSyntax::RequiredRef(
112                                RequiredRefSyntax {
113                                    name: Cow::Borrowed("array"),
114                                    require_array: true,
115                                    define_undefined: false,
116                                },
117                                ArgSepSyntax::Exactly(ArgSep::Long),
118                            ),
119                            SingularArgSyntax::RequiredValue(
120                                RequiredValueSyntax {
121                                    name: Cow::Borrowed("dimension"),
122                                    vtype: ExprType::Integer,
123                                },
124                                ArgSepSyntax::End,
125                            ),
126                        ],
127                        None,
128                    ),
129                ])
130                .with_category(CATEGORY)
131                .with_description(
132                    "Returns the lower bound for the given dimension of the array.
133The lower bound is the smallest available subscript that can be provided to array indexing \
134operations.
135For one-dimensional arrays, the dimension% is optional.  For multi-dimensional arrays, the \
136dimension% is a 1-indexed integer.",
137                )
138                .build(),
139        })
140    }
141}
142
143#[async_trait(?Send)]
144impl Callable for LboundFunction {
145    fn metadata(&self) -> &CallableMetadata {
146        &self.metadata
147    }
148
149    async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
150        let (_array, _dim) = parse_bound_args(&mut scope, machine.get_symbols())?;
151        scope.return_integer(0)
152    }
153}
154
155/// The `UBOUND` function.
156pub struct UboundFunction {
157    metadata: CallableMetadata,
158}
159
160impl UboundFunction {
161    /// Creates a new instance of the function.
162    pub fn new() -> Rc<Self> {
163        Rc::from(Self {
164            metadata: CallableMetadataBuilder::new("UBOUND")
165                .with_return_type(ExprType::Integer)
166                .with_syntax(&[
167                    (
168                        &[SingularArgSyntax::RequiredRef(
169                            RequiredRefSyntax {
170                                name: Cow::Borrowed("array"),
171                                require_array: true,
172                                define_undefined: false,
173                            },
174                            ArgSepSyntax::End,
175                        )],
176                        None,
177                    ),
178                    (
179                        &[
180                            SingularArgSyntax::RequiredRef(
181                                RequiredRefSyntax {
182                                    name: Cow::Borrowed("array"),
183                                    require_array: true,
184                                    define_undefined: false,
185                                },
186                                ArgSepSyntax::Exactly(ArgSep::Long),
187                            ),
188                            SingularArgSyntax::RequiredValue(
189                                RequiredValueSyntax {
190                                    name: Cow::Borrowed("dimension"),
191                                    vtype: ExprType::Integer,
192                                },
193                                ArgSepSyntax::End,
194                            ),
195                        ],
196                        None,
197                    ),
198                ])
199                .with_category(CATEGORY)
200                .with_description(
201                    "Returns the upper bound for the given dimension of the array.
202The upper bound is the largest available subscript that can be provided to array indexing \
203operations.
204For one-dimensional arrays, the dimension% is optional.  For multi-dimensional arrays, the \
205dimension% is a 1-indexed integer.",
206                )
207                .build(),
208        })
209    }
210}
211
212#[async_trait(?Send)]
213impl Callable for UboundFunction {
214    fn metadata(&self) -> &CallableMetadata {
215        &self.metadata
216    }
217
218    async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
219        let (array, dim) = parse_bound_args(&mut scope, machine.get_symbols())?;
220        scope.return_integer((array.dimensions()[dim - 1] - 1) as i32)
221    }
222}
223
224/// Adds all symbols provided by this module to the given `machine`.
225pub fn add_all(machine: &mut Machine) {
226    machine.add_callable(LboundFunction::new());
227    machine.add_callable(UboundFunction::new());
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::testutils::*;
234
235    /// Validates error handling of `LBOUND` and `UBOUND` as given in `func`.
236    fn do_bound_errors_test(func: &str) {
237        Tester::default()
238            .run(format!("DIM x(2): result = {}()", func))
239            .expect_compilation_err(format!(
240                "1:20: In call to {}: expected <array> | <array, dimension%>",
241                func
242            ))
243            .check();
244
245        Tester::default()
246            .run(format!("DIM x(2): result = {}(x, 1, 2)", func))
247            .expect_compilation_err(format!(
248                "1:20: In call to {}: expected <array> | <array, dimension%>",
249                func
250            ))
251            .check();
252
253        Tester::default()
254            .run(format!("DIM x(2): result = {}(x, -1)", func))
255            .expect_err(format!("1:20: In call to {}: 1:30: Dimension -1 must be positive", func))
256            .expect_array("x", ExprType::Integer, &[2], vec![])
257            .check();
258
259        Tester::default()
260            .run(format!("DIM x(2): result = {}(x, TRUE)", func))
261            .expect_compilation_err(format!(
262                "1:20: In call to {}: 1:30: BOOLEAN is not a number",
263                func
264            ))
265            .check();
266
267        Tester::default()
268            .run(format!("i = 0: result = {}(i)", func))
269            .expect_compilation_err(format!(
270                "1:17: In call to {}: 1:24: i is not an array reference",
271                func
272            ))
273            .check();
274
275        Tester::default()
276            .run(format!("result = {}(3)", func))
277            .expect_compilation_err(format!(
278                "1:10: In call to {}: 1:17: Requires an array reference, not a value",
279                func
280            ))
281            .check();
282
283        Tester::default()
284            .run(format!("i = 0: result = {}(i)", func))
285            .expect_compilation_err(format!(
286                "1:17: In call to {}: 1:24: i is not an array reference",
287                func
288            ))
289            .check();
290
291        Tester::default()
292            .run(format!("DIM i(3) AS BOOLEAN: result = {}(i$)", func))
293            .expect_compilation_err(format!(
294                "1:31: In call to {}: 1:38: Incompatible type annotation in i$ reference",
295                func
296            ))
297            .check();
298
299        Tester::default()
300            .run(format!("result = {}(x)", func))
301            .expect_compilation_err(format!("1:10: In call to {}: 1:17: Undefined array x", func))
302            .check();
303
304        Tester::default()
305            .run(format!("DIM x(2, 3, 4): result = {}(x)", func))
306            .expect_err(format!(
307                "1:26: In call to {}: 1:33: Requires a dimension for multidimensional arrays",
308                func
309            ))
310            .expect_array("x", ExprType::Integer, &[2, 3, 4], vec![])
311            .check();
312
313        Tester::default()
314            .run(format!("DIM x(2, 3, 4): result = {}(x, 5)", func))
315            .expect_err(format!(
316                "1:26: In call to {}: 1:36: Array X has only 3 dimensions but asked for 5",
317                func
318            ))
319            .expect_array("x", ExprType::Integer, &[2, 3, 4], vec![])
320            .check();
321    }
322
323    #[test]
324    fn test_lbound_ok() {
325        Tester::default()
326            .run("DIM x(10): result = LBOUND(x)")
327            .expect_var("result", 0i32)
328            .expect_array("x", ExprType::Integer, &[10], vec![])
329            .check();
330
331        Tester::default()
332            .run("DIM x(10, 20): result = LBOUND(x, 1)")
333            .expect_var("result", 0i32)
334            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
335            .check();
336
337        Tester::default()
338            .run("DIM x(10, 20): result = LBOUND(x, 2.1)")
339            .expect_var("result", 0i32)
340            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
341            .check();
342    }
343
344    #[test]
345    fn test_lbound_errors() {
346        do_bound_errors_test("LBOUND");
347    }
348
349    #[test]
350    fn test_ubound_ok() {
351        Tester::default()
352            .run("DIM x(10): result = UBOUND(x)")
353            .expect_var("result", 9i32)
354            .expect_array("x", ExprType::Integer, &[10], vec![])
355            .check();
356
357        Tester::default()
358            .run("DIM x(10, 20): result = UBOUND(x, 1)")
359            .expect_var("result", 9i32)
360            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
361            .check();
362
363        Tester::default()
364            .run("DIM x(10, 20): result = UBOUND(x, 2.1)")
365            .expect_var("result", 19i32)
366            .expect_array("x", ExprType::Integer, &[10, 20], vec![])
367            .check();
368    }
369
370    #[test]
371    fn test_ubound_errors() {
372        do_bound_errors_test("UBOUND");
373    }
374
375    #[test]
376    fn test_bound_integration() {
377        Tester::default()
378            .run("DIM x(5): FOR i = LBOUND(x) TO UBOUND(x): x(i) = i * 2: NEXT")
379            .expect_var("i", 5i32)
380            .expect_array_simple(
381                "x",
382                ExprType::Integer,
383                vec![0i32.into(), 2i32.into(), 4i32.into(), 6i32.into(), 8i32.into()],
384            )
385            .check();
386    }
387}