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::{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
31const CATEGORY: &str = "Array functions";
33
34#[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
86pub struct LboundFunction {
88 metadata: CallableMetadata,
89}
90
91impl LboundFunction {
92 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
155pub struct UboundFunction {
157 metadata: CallableMetadata,
158}
159
160impl UboundFunction {
161 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
224pub 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 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}