Skip to main content

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