Skip to main content

endbasic_std/
data.rs

1// EndBASIC
2// Copyright 2022 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//! Commands to interact with the data provided by `DATA` statements.
18
19use crate::numerics::double_to_integer;
20use crate::{Clearable, MachineBuilder};
21use async_trait::async_trait;
22use endbasic_core::{
23    ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
24    CallableMetadataBuilder, ConstantDatum, ExprType, RepeatedSyntax, RepeatedTypeSyntax, Scope,
25    VarArgTag,
26};
27use std::borrow::Cow;
28use std::cell::RefCell;
29use std::rc::Rc;
30
31/// Category description for all symbols provided by this module.
32pub(crate) const CATEGORY: &str = "Data management";
33
34struct ClearableIndex(Rc<RefCell<usize>>);
35
36impl Clearable for ClearableIndex {
37    fn reset_state(&self) {
38        *self.0.borrow_mut() = 0;
39    }
40}
41
42fn expr_type_name(vtype: ExprType) -> &'static str {
43    match vtype {
44        ExprType::Boolean => "BOOLEAN",
45        ExprType::Double => "DOUBLE",
46        ExprType::Integer => "INTEGER",
47        ExprType::Text => "STRING",
48    }
49}
50
51fn value_type_name(value: &ConstantDatum) -> &'static str {
52    match value {
53        ConstantDatum::Boolean(..) => "BOOLEAN",
54        ConstantDatum::Double(..) => "DOUBLE",
55        ConstantDatum::Integer(..) => "INTEGER",
56        ConstantDatum::Text(..) => "STRING",
57    }
58}
59
60/// The `READ` command.
61pub struct ReadCommand {
62    metadata: Rc<CallableMetadata>,
63    index: Rc<RefCell<usize>>,
64}
65
66impl ReadCommand {
67    /// Creates a new `READ` command.
68    pub fn new(index: Rc<RefCell<usize>>) -> Rc<Self> {
69        Rc::from(Self {
70            metadata: CallableMetadataBuilder::new("READ")
71                .with_syntax(&[(
72                    &[],
73                    Some(&RepeatedSyntax {
74                        name: Cow::Borrowed("vref"),
75                        type_syn: RepeatedTypeSyntax::VariableRef,
76                        sep: ArgSepSyntax::Exactly(ArgSep::Long),
77                        require_one: true,
78                        allow_missing: false,
79                    }),
80                )])
81                .with_category(CATEGORY)
82                .with_description(
83                    "Extracts data values from DATA statements.
84DATA statements can appear anywhere in the program and they register data values into an array of \
85values.  READ is then used to extract values from this array into the provided variables in the \
86order in which they were defined.  In other words: READ maintains an internal index into the data \
87array that gets incremented by the number of provided variables every time it is executed.
88The variable references in the vref1..vrefN list must match the types or be compatible with the \
89values in the corresponding position of the data array.  Empty values in the data array can be \
90specified by DATA, and those are converted into the default values for the relevant types: \
91booleans are false, numbers are 0, and strings are empty.
92Attempting to extract more values than are defined by DATA results in an \"out of data\" error.
93The index that READ uses to extract DATA values can be reset by RESTORE and, more generally, by \
94CLEAR.",
95                )
96                .build(),
97            index,
98        })
99    }
100}
101
102#[async_trait(?Send)]
103impl Callable for ReadCommand {
104    fn metadata(&self) -> Rc<CallableMetadata> {
105        self.metadata.clone()
106    }
107
108    fn exec(&self, mut scope: Scope<'_>) -> CallResult<()> {
109        debug_assert_ne!(0, scope.nargs());
110
111        fn assign_datum(scope: &mut Scope<'_>, reg: u8, datum: ConstantDatum) -> CallResult<()> {
112            let target_type = scope.get_ref(reg).vtype;
113            match (target_type, datum) {
114                (ExprType::Boolean, ConstantDatum::Boolean(b)) => {
115                    scope.get_mut_ref(reg).set_boolean(b);
116                    Ok(())
117                }
118                (ExprType::Double, ConstantDatum::Double(d)) => {
119                    scope.get_mut_ref(reg).set_double(d);
120                    Ok(())
121                }
122                (ExprType::Double, ConstantDatum::Integer(i)) => {
123                    scope.get_mut_ref(reg).set_double(i as f64);
124                    Ok(())
125                }
126                (ExprType::Integer, ConstantDatum::Double(d)) => {
127                    let i = double_to_integer(d)
128                        .map_err(|e| CallError::Syntax(scope.get_pos(reg), e.to_string()))?;
129                    scope.get_mut_ref(reg).set_integer(i);
130                    Ok(())
131                }
132                (ExprType::Integer, ConstantDatum::Integer(i)) => {
133                    scope.get_mut_ref(reg).set_integer(i);
134                    Ok(())
135                }
136                (ExprType::Text, ConstantDatum::Text(s)) => {
137                    scope.get_mut_ref(reg).set_string(s)?;
138                    Ok(())
139                }
140                (target_type, source_value) => Err(CallError::Syntax(
141                    scope.get_pos(reg),
142                    format!(
143                        "Cannot assign value of type {} to variable of type {}",
144                        value_type_name(&source_value),
145                        expr_type_name(target_type),
146                    ),
147                )),
148            }
149        }
150
151        let mut index = self.index.borrow_mut();
152        let mut reg = 0;
153        loop {
154            let sep = if let VarArgTag::Pointer(sep) = scope.get_type(reg) {
155                sep
156            } else {
157                unreachable!();
158            };
159            reg += 1;
160
161            let vtype = scope.get_ref(reg).vtype;
162            let datum = match scope.data().get(*index) {
163                None => {
164                    return Err(CallError::Syntax(scope.get_pos(reg), "Out of data".to_owned()));
165                }
166                Some(Some(datum)) => datum.clone(),
167                Some(None) => match vtype {
168                    ExprType::Boolean => ConstantDatum::Boolean(false),
169                    ExprType::Double => ConstantDatum::Double(0.0),
170                    ExprType::Integer => ConstantDatum::Integer(0),
171                    ExprType::Text => ConstantDatum::Text(String::new()),
172                },
173            };
174            *index += 1;
175            assign_datum(&mut scope, reg, datum)?;
176            reg += 1;
177
178            if sep == ArgSep::End {
179                break;
180            }
181        }
182
183        Ok(())
184    }
185}
186
187/// The `RESTORE` command.
188pub struct RestoreCommand {
189    metadata: Rc<CallableMetadata>,
190    index: Rc<RefCell<usize>>,
191}
192
193impl RestoreCommand {
194    /// Creates a new `RESTORE` command.
195    pub fn new(index: Rc<RefCell<usize>>) -> Rc<Self> {
196        Rc::from(Self {
197            metadata: CallableMetadataBuilder::new("RESTORE")
198                .with_syntax(&[(&[], None)])
199                .with_category(CATEGORY)
200                .with_description(
201                    "Resets the index of the data element to be returned.
202This allows READ to re-return the same elements that were previously extracted from the array of \
203values defined by DATA.",
204                )
205                .build(),
206            index,
207        })
208    }
209}
210
211impl Callable for RestoreCommand {
212    fn metadata(&self) -> Rc<CallableMetadata> {
213        self.metadata.clone()
214    }
215
216    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
217        debug_assert_eq!(0, scope.nargs());
218        *self.index.borrow_mut() = 0;
219        Ok(())
220    }
221}
222
223/// Instantiates all symbols in this module and adds them to the `machine`.
224pub fn add_all(machine: &mut MachineBuilder) {
225    let index = Rc::from(RefCell::from(0));
226    machine.add_clearable(Box::from(ClearableIndex(index.clone())));
227    machine.add_callable(ReadCommand::new(index.clone()));
228    machine.add_callable(RestoreCommand::new(index));
229}
230
231#[cfg(test)]
232mod tests {
233    use crate::testutils::*;
234
235    #[test]
236    fn test_read_simple() {
237        Tester::default()
238            .run(
239                r#"
240            READ i: PRINT i
241            READ i: PRINT i
242            DATA 3, 5, -7
243            READ i: PRINT i
244            "#,
245            )
246            .expect_prints([" 3", " 5", "-7"])
247            .expect_var("I", -7)
248            .check();
249    }
250
251    #[test]
252    fn test_read_multiple() {
253        Tester::default()
254            .run(
255                r#"
256            READ i, j, k, i
257            DATA 3, 5, 7, 6
258            "#,
259            )
260            .expect_var("I", 6)
261            .expect_var("J", 5)
262            .expect_var("K", 7)
263            .check();
264    }
265
266    #[test]
267    fn test_read_defaults_with_annotations() {
268        Tester::default()
269            .run(r#"DATA , , , ,: READ a, b?, d#, i%, s$"#)
270            .expect_var("a", 0)
271            .expect_var("b", false)
272            .expect_var("d", 0.0)
273            .expect_var("i", 0)
274            .expect_var("s", "")
275            .check();
276    }
277
278    #[test]
279    fn test_read_defaults_without_annotations() {
280        Tester::default()
281            .run(
282                r#"
283            DIM SHARED b AS BOOLEAN
284            DIM SHARED d AS DOUBLE
285            DIM SHARED i AS INTEGER
286            DIM SHARED s AS STRING
287            DATA , , , ,
288            READ a, b, d, i, s
289            "#,
290            )
291            .expect_var("a", 0)
292            .expect_var("b", false)
293            .expect_var("d", 0.0)
294            .expect_var("i", 0)
295            .expect_var("s", "")
296            .check();
297    }
298
299    #[test]
300    fn test_read_double_to_integer() {
301        Tester::default().run(r#"DATA 5.6: READ i%"#).expect_var("i", 6).check();
302    }
303
304    #[test]
305    fn test_read_integer_to_double() {
306        Tester::default().run(r#"DATA 5: READ d#"#).expect_var("d", 5.0).check();
307    }
308
309    #[test]
310    fn test_read_out_of_data() {
311        Tester::default()
312            .run(r#"DATA 5: READ i: READ j"#)
313            .expect_err("1:22: Out of data")
314            .expect_var("I", 5)
315            .check();
316    }
317
318    #[test]
319    fn test_read_clear_on_run() {
320        Tester::default()
321            .set_program(None, "DATA 1: READ i: PRINT i")
322            .run(r#"RUN: RUN"#)
323            .expect_clear()
324            .expect_prints([" 1"])
325            .expect_var("I", 1)
326            .expect_program(None as Option<String>, "DATA 1: READ i: PRINT i")
327            .check();
328    }
329
330    #[test]
331    fn test_read_index_remains_out_of_bounds() {
332        let mut t = Tester::default();
333        t.run(r#"DATA 1: READ i, j"#).expect_var("i", 1).expect_err("1:17: Out of data").check();
334
335        // This represents a second invocation in the REPL, and the read index is reset before this
336        // execution, so the first and second data values are returned as expected.
337        t.run(r#"DATA 1, 2: READ i, j"#).expect_var("i", 1).expect_var("j", 2).check();
338
339        // Running `CLEAR` explicitly should resolve the issue described above and give us the
340        // expected behavior.
341        t.run(r#"CLEAR"#).expect_clear().check();
342        t.run(r#"DATA 1, 2: READ i, j"#)
343            .expect_clear()
344            .expect_var("i", 1)
345            .expect_var("j", 2)
346            .check();
347    }
348
349    #[test]
350    fn test_read_errors() {
351        check_stmt_compilation_err("1:1: READ expected vref1[, .., vrefN]", "READ");
352        check_stmt_compilation_err("1:6: READ expected vref1[, .., vrefN]", "READ 3");
353        check_stmt_compilation_err("1:7: READ expected vref1[, .., vrefN]", "READ i; j");
354
355        check_stmt_err(
356            "1:34: Cannot assign value of type STRING to variable of type INTEGER",
357            "DIM i AS INTEGER: DATA \"x\": READ i",
358        );
359        check_stmt_err(
360            "1:36: Cannot assign value of type BOOLEAN to variable of type INTEGER",
361            "DIM s AS INTEGER: DATA FALSE: READ s",
362        );
363    }
364
365    #[test]
366    fn test_restore_nothing() {
367        Tester::default().run("RESTORE").check();
368    }
369
370    #[test]
371    fn test_restore_before_read() {
372        Tester::default()
373            .run(
374                r#"
375            DATA 3, 5, 7
376            RESTORE
377            READ i: PRINT i
378            READ i: PRINT i
379            "#,
380            )
381            .expect_prints([" 3", " 5"])
382            .expect_var("I", 5)
383            .check();
384    }
385
386    #[test]
387    fn test_restore_after_read() {
388        Tester::default()
389            .run(
390                r#"
391            DATA 3, -5, 7
392            READ i: PRINT i
393            READ i: PRINT i
394            RESTORE
395            READ i: PRINT i
396            "#,
397            )
398            .expect_prints([" 3", "-5", " 3"])
399            .expect_var("I", 3)
400            .check();
401    }
402
403    #[test]
404    fn test_restore_errors() {
405        check_stmt_compilation_err("1:1: RESTORE expected no arguments", "RESTORE 123");
406    }
407}