1use 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
30const CATEGORY: &str = "Array functions";
32
33#[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
80pub struct LboundFunction {
82 metadata: CallableMetadata,
83}
84
85impl LboundFunction {
86 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
149pub struct UboundFunction {
151 metadata: CallableMetadata,
152}
153
154impl UboundFunction {
155 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
218pub 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 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}