1use 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
29const CATEGORY: &str = "Array functions";
31
32fn 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
69pub struct LboundFunction {
71 metadata: Rc<CallableMetadata>,
72}
73
74impl LboundFunction {
75 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
137pub struct UboundFunction {
139 metadata: Rc<CallableMetadata>,
140}
141
142impl UboundFunction {
143 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
205pub 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 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}