Skip to main content

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