diff --git a/.github/workflows/lint.sh b/.github/workflows/lint.sh index 4b02c77c..32396872 100755 --- a/.github/workflows/lint.sh +++ b/.github/workflows/lint.sh @@ -58,7 +58,9 @@ check_web_versions() { check_rust() { cargo clippy --all-targets -- -D warnings + cargo clippy --all-targets --all -- -D warnings cargo clippy --all-targets --all-features -- -D warnings + cargo clippy --all-targets --all-features --all -- -D warnings cargo fmt -- --check } diff --git a/Cargo.toml b/Cargo.toml index 12c29593..c188f977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [workspace] +resolver = "3" members = [ "cli", "client", "core", + "core2", "repl", "rpi", "sdl", @@ -17,3 +19,17 @@ default-members = [ "repl", "std", ] + +[workspace.lints.rust] +anonymous_parameters = "warn" +bad_style = "warn" +missing_docs = "warn" +unused = "warn" +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_qualifications = "warn" +unsafe_code = "warn" + +[workspace.lints.clippy] +await_holding_refcell_ref = "allow" +collapsible_else_if = "allow" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 86d9412c..c3c5b3a8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - CLI" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [features] default = ["crossterm"] diff --git a/cli/build.rs b/cli/build.rs index cdf1464e..62c675d0 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -13,6 +13,8 @@ // License for the specific language governing permissions and limitations // under the License. +//! Build script for the crate. + use std::env; fn main() { diff --git a/cli/src/main.rs b/cli/src/main.rs index 3f16707c..9b0c18e9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -15,14 +15,7 @@ //! Command-line interface for the EndBASIC language. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use async_channel::Sender; use endbasic_core::exec::Signal; use endbasic_std::console::{Console, ConsoleSpec}; @@ -218,7 +211,7 @@ fn setup_console( return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Unknown console driver {}", driver), - )) + )); } }; console_spec.finish().map_err(|e| { diff --git a/cli/tests/integration_test.rs b/cli/tests/integration_test.rs index cc1d98df..c5e239ac 100644 --- a/cli/tests/integration_test.rs +++ b/cli/tests/integration_test.rs @@ -15,11 +15,6 @@ //! Integration tests that use golden input and output files. -// Keep these in sync with other top-level files. -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use std::env; use std::fs::{self, File}; use std::io::Read; diff --git a/client/Cargo.toml b/client/Cargo.toml index 65d0063d..77f32c42 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - cloud service client" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-trait = "0.1" diff --git a/client/src/cloud.rs b/client/src/cloud.rs index 8749e1f1..112c421a 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -21,9 +21,9 @@ use base64::prelude::*; use bytes::Buf; use endbasic_std::console::remove_control_chars; use endbasic_std::storage::FileAcls; -use reqwest::header::HeaderMap; use reqwest::Response; use reqwest::StatusCode; +use reqwest::header::HeaderMap; use std::cell::RefCell; use std::io; use std::rc::Rc; @@ -103,7 +103,7 @@ impl CloudService { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Invalid base API address: {}", e), - )) + )); } }; @@ -288,7 +288,7 @@ impl Service for CloudService { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("Server returned invalid reader ACL: {}", e), - )) + )); } } } @@ -613,7 +613,7 @@ mod tests { env::var("TEST_ACCOUNT_1_USERNAME").expect("Expected env config not found"); let password = "this is an invalid password for the test account"; - let err = context.service.login(&username, &password).await.unwrap_err(); + let err = context.service.login(&username, password).await.unwrap_err(); assert_eq!(io::ErrorKind::PermissionDenied, err.kind()); } run(&mut TestContext::new_from_env()); diff --git a/client/src/cmds.rs b/client/src/cmds.rs index 9abb0c97..fe4b6426 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -17,14 +17,14 @@ use crate::*; use async_trait::async_trait; +use endbasic_core::LineCol; use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredValueSyntax, SingularArgSyntax, }; use endbasic_core::exec::{Error, Machine, Result, Scope}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; -use endbasic_core::LineCol; -use endbasic_std::console::{is_narrow, read_line, read_line_secure, refill_and_print, Console}; +use endbasic_std::console::{Console, is_narrow, read_line, read_line_secure, refill_and_print}; use endbasic_std::storage::{FileAcls, Storage}; use endbasic_std::strings::parse_boolean; use std::borrow::Cow; @@ -216,7 +216,7 @@ impl Callable for LogoutCommand { Err(e) => { return Err( scope.io_error(io::Error::new(e.kind(), format!("Cannot log out: {}", e))) - ) + ); } }; @@ -320,7 +320,7 @@ impl ShareCommand { "Invalid ACL '{}{}': must be of the form \"username+r\" or \"username-r\"", username, change ), - )) + )); } } Ok(()) diff --git a/client/src/lib.rs b/client/src/lib.rs index c569f7c2..d956e5a4 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -15,13 +15,6 @@ //! EndBASIC service client. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use async_trait::async_trait; use endbasic_std::storage::{DiskSpace, FileAcls}; use serde::{Deserialize, Serialize}; diff --git a/client/src/testutils.rs b/client/src/testutils.rs index 6d2faa30..a14af54e 100644 --- a/client/src/testutils.rs +++ b/client/src/testutils.rs @@ -15,7 +15,7 @@ //! Test utilities for the cloud service. -use crate::{add_all, AccessToken, GetFilesResponse, LoginResponse, Service, SignupRequest}; +use crate::{AccessToken, GetFilesResponse, LoginResponse, Service, SignupRequest, add_all}; use async_trait::async_trait; use endbasic_std::storage::{FileAcls, Storage}; use endbasic_std::testutils::*; @@ -27,6 +27,7 @@ use std::rc::Rc; /// Service client implementation that allows specifying expectations on requests and yields the /// responses previously recorded into it. #[derive(Default)] +#[allow(clippy::type_complexity)] pub struct MockService { access_token: Option, @@ -141,8 +142,7 @@ impl MockService { let exp_remove = FileAcls { readers: exp_remove.into().into_iter().map(|v| v.into()).collect::>(), }; - let exp_request = - (username.to_owned(), filename.to_owned(), exp_add.into(), exp_remove.into()); + let exp_request = (username.to_owned(), filename.to_owned(), exp_add, exp_remove); self.mock_patch_file_acls.push_back((exp_request, result)); } @@ -182,8 +182,8 @@ impl Service for MockService { async fn login(&mut self, username: &str, password: &str) -> io::Result { let mock = self.mock_login.pop_front().expect("No mock requests available"); - assert_eq!(&mock.0 .0, username); - assert_eq!(&mock.0 .1, password); + assert_eq!(&mock.0.0, username); + assert_eq!(&mock.0.1, password); if let Ok(response) = &mock.1 { self.access_token = Some(response.access_token.clone()); @@ -203,10 +203,7 @@ impl Service for MockService { } fn logged_in_username(&self) -> Option { - match self.access_token { - Some(_) => Some("logged-in-username".to_owned()), - None => None, - } + self.access_token.as_ref().map(|_| "logged-in-username".to_owned()) } async fn get_files(&mut self, username: &str) -> io::Result { @@ -220,8 +217,8 @@ impl Service for MockService { self.access_token.as_ref().expect("login not called yet"); let mock = self.mock_get_file.pop_front().expect("No mock requests available"); - assert_eq!(&mock.0 .0, username); - assert_eq!(&mock.0 .1, filename); + assert_eq!(&mock.0.0, username); + assert_eq!(&mock.0.1, filename); mock.1 } @@ -229,8 +226,8 @@ impl Service for MockService { self.access_token.as_ref().expect("login not called yet"); let mock = self.mock_get_file_acls.pop_front().expect("No mock requests available"); - assert_eq!(&mock.0 .0, username); - assert_eq!(&mock.0 .1, filename); + assert_eq!(&mock.0.0, username); + assert_eq!(&mock.0.1, filename); mock.1 } @@ -243,9 +240,9 @@ impl Service for MockService { self.access_token.as_ref().expect("login not called yet"); let mock = self.mock_patch_file_content.pop_front().expect("No mock requests available"); - assert_eq!(&mock.0 .0, username); - assert_eq!(&mock.0 .1, filename); - assert_eq!(&mock.0 .2, &content); + assert_eq!(&mock.0.0, username); + assert_eq!(&mock.0.1, filename); + assert_eq!(&mock.0.2, &content); mock.1 } @@ -259,10 +256,10 @@ impl Service for MockService { self.access_token.as_ref().expect("login not called yet"); let mock = self.mock_patch_file_acls.pop_front().expect("No mock requests available"); - assert_eq!(&mock.0 .0, username); - assert_eq!(&mock.0 .1, filename); - assert_eq!(&mock.0 .2, add); - assert_eq!(&mock.0 .3, remove); + assert_eq!(&mock.0.0, username); + assert_eq!(&mock.0.1, filename); + assert_eq!(&mock.0.2, add); + assert_eq!(&mock.0.3, remove); mock.1 } @@ -270,8 +267,8 @@ impl Service for MockService { self.access_token.as_ref().expect("login not called yet"); let mock = self.mock_delete_file.pop_front().expect("No mock requests available"); - assert_eq!(&mock.0 .0, username); - assert_eq!(&mock.0 .1, filename); + assert_eq!(&mock.0.0, username); + assert_eq!(&mock.0.1, filename); mock.1 } } diff --git a/core/Cargo.toml b/core/Cargo.toml index 28ec89ad..50d3b1cf 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - core" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-channel = "2.2" diff --git a/core/src/ast.rs b/core/src/ast.rs index 26b8419b..b5d13439 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -15,7 +15,7 @@ //! Abstract Syntax Tree (AST) for the EndBASIC language. -use crate::{reader::LineCol, syms::SymbolKey}; +use crate::reader::LineCol; use std::fmt; /// Components of a boolean literal expression. @@ -252,12 +252,12 @@ impl fmt::Display for ExprType { #[derive(Clone, Debug, Eq, PartialEq)] pub struct VarRef { /// Name of the variable this points to. - name: String, + pub name: String, /// Type of the variable this points to, if explicitly specified. /// /// If `None`, the type of the variable is subject to type inference. - ref_type: Option, + pub ref_type: Option, } impl VarRef { @@ -266,22 +266,6 @@ impl VarRef { Self { name: name.into(), ref_type } } - /// Returns the name of this reference, without any type annotations. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the name of this reference, without any type annotations, and consumes the - /// reference. - pub(crate) fn take_name(self) -> String { - self.name - } - - /// Returns the type of this reference. - pub fn ref_type(&self) -> Option { - self.ref_type - } - /// Returns true if this reference is compatible with the given type. pub fn accepts(&self, other: ExprType) -> bool { match self.ref_type { @@ -300,11 +284,6 @@ impl VarRef { }, } } - - /// Converts this variable reference to a symbol key. - pub(crate) fn as_symbol_key(&self) -> SymbolKey { - SymbolKey::from(&self.name) - } } impl fmt::Display for VarRef { @@ -332,7 +311,7 @@ pub enum Value { Text(String), // Should be `String` but would get confusing with the built-in Rust type. /// A reference to a variable. - VarRef(SymbolKey, ExprType), + VarRef(String, ExprType), } impl From for Value { @@ -517,7 +496,7 @@ pub struct CallableSpan { #[derive(Debug, PartialEq)] pub struct DataSpan { /// Collection of optional literal values. - pub values: Vec>, + pub values: Vec>, } /// Components of a variable definition. diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 9c74204c..1c1a8be0 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -339,7 +339,7 @@ fn compile_required_ref( ) -> Result> { match expr { Some(Expr::Symbol(span)) => { - let key = SymbolKey::from(span.vref.name()); + let key = SymbolKey::from(&span.vref.name); match symtable.get(&key) { None => { if !define_undefined { @@ -347,7 +347,7 @@ fn compile_required_ref( } debug_assert!(!require_array); - let vtype = span.vref.ref_type().unwrap_or(ExprType::Integer); + let vtype = span.vref.ref_type.unwrap_or(ExprType::Integer); if !span.vref.accepts(vtype) { return Err(Error::IncompatibleTypeAnnotationInReference( diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index a6c5caea..1eae2951 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -352,7 +352,7 @@ fn compile_expr_symbol( span: SymbolSpan, allow_varrefs: bool, ) -> Result { - let key = SymbolKey::from(span.vref.name()); + let key = SymbolKey::from(&span.vref.name); let (instr, vtype) = match symtable.get(&key) { None => return Err(Error::UndefinedSymbol(span.pos, key)), @@ -699,7 +699,7 @@ pub(super) fn compile_expr( Expr::Negate(span) => Ok(compile_neg_op(instrs, fixups, symtable, *span)?), Expr::Call(span) => { - let key = SymbolKey::from(span.vref.name()); + let key = SymbolKey::from(&span.vref.name); match symtable.get(&key) { Some(SymbolPrototype::Array(vtype, dims)) => { compile_array_ref(instrs, fixups, symtable, span, key, *vtype, *dims) @@ -810,8 +810,8 @@ mod tests { use super::*; use crate::bytecode::{BuiltinCallISpan, FunctionCallISpan}; use crate::compiler::{ - testutils::*, ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax, - RequiredValueSyntax, SingularArgSyntax, + ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax, RequiredValueSyntax, + SingularArgSyntax, testutils::*, }; use crate::syms::CallableMetadataBuilder; use std::borrow::Cow; diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 7dbd1cad..f6512133 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -354,7 +354,7 @@ struct Compiler { instrs: Vec, /// Data discovered so far. - data: Vec>, + data: Vec>, /// Symbols table. symtable: SymbolsTable, @@ -455,11 +455,11 @@ impl Compiler { /// Compiles an assignment to an array position. fn compile_array_assignment(&mut self, span: ArrayAssignmentSpan) -> Result<()> { - let key = SymbolKey::from(span.vref.name()); + let key = SymbolKey::from(&span.vref.name); let (atype, dims) = match self.symtable.get(&key) { Some(SymbolPrototype::Array(atype, dims)) => (*atype, *dims), Some(_) => { - return Err(Error::IndexNonArray(span.vref_pos, span.vref.take_name())); + return Err(Error::IndexNonArray(span.vref_pos, span.vref.name)); } None => { return Err(Error::UndefinedSymbol(span.vref_pos, key)); @@ -489,13 +489,13 @@ impl Compiler { /// It's important to always use this function instead of manually emitting `Instruction::Assign` /// instructions to ensure consistent handling of the symbols table. fn compile_assignment(&mut self, vref: VarRef, vref_pos: LineCol, expr: Expr) -> Result<()> { - let mut key = SymbolKey::from(&vref.name()); + let mut key = SymbolKey::from(&vref.name); let etype = self.compile_expr(expr)?; - if let Some(current_callable) = self.current_callable.as_ref() { - if key == current_callable.0 { - key = Compiler::return_key(¤t_callable.0); - } + if let Some(current_callable) = self.current_callable.as_ref() + && key == current_callable.0 + { + key = Compiler::return_key(¤t_callable.0); } let vtype = match self.symtable.get(&key) { @@ -505,7 +505,7 @@ impl Compiler { // TODO(jmmv): Compile separate Dim instructions for new variables instead of // checking this every time. let key = key.clone(); - let vtype = vref.ref_type().unwrap_or(etype); + let vtype = vref.ref_type.unwrap_or(etype); self.symtable.insert(key, SymbolPrototype::Variable(vtype)); vtype } @@ -516,10 +516,10 @@ impl Compiler { return Err(Error::IncompatibleTypesInAssignment(vref_pos, etype, vtype)); } - if let Some(ref_type) = vref.ref_type() { - if ref_type != vtype { - return Err(Error::IncompatibleTypeAnnotationInReference(vref_pos, vref)); - } + if let Some(ref_type) = vref.ref_type + && ref_type != vtype + { + return Err(Error::IncompatibleTypeAnnotationInReference(vref_pos, vref)); } self.emit(Instruction::Assign(key)); @@ -529,7 +529,7 @@ impl Compiler { /// Compiles a `FUNCTION` or `SUB` definition. fn compile_callable(&mut self, span: CallableSpan) -> Result<()> { - let key = SymbolKey::from(span.name.name()); + let key = SymbolKey::from(&span.name.name); if self.symtable.contains_key(&key) { return Err(Error::RedefinitionError(span.name_pos, key)); } @@ -543,16 +543,16 @@ impl Compiler { }; syntax.push(SingularArgSyntax::RequiredValue( RequiredValueSyntax { - name: Cow::Owned(param.name().to_owned()), - vtype: param.ref_type().unwrap_or(ExprType::Integer), + name: Cow::Owned(param.name.to_owned()), + vtype: param.ref_type.unwrap_or(ExprType::Integer), }, sep, )); } - let mut builder = CallableMetadataBuilder::new_dynamic(span.name.name().to_owned()) + let mut builder = CallableMetadataBuilder::new_dynamic(span.name.name.to_owned()) .with_dynamic_syntax(vec![(syntax, None)]); - if let Some(ctype) = span.name.ref_type() { + if let Some(ctype) = span.name.ref_type { builder = builder.with_return_type(ctype); } self.symtable.insert_global(key, SymbolPrototype::Callable(builder.build())); @@ -641,29 +641,28 @@ impl Compiler { /// Compiles a `FOR` loop and appends its instructions to the compilation context. fn compile_for(&mut self, span: ForSpan) -> Result<()> { debug_assert!( - span.iter.ref_type().is_none() - || span.iter.ref_type().unwrap() == ExprType::Double - || span.iter.ref_type().unwrap() == ExprType::Integer + span.iter.ref_type.is_none() + || span.iter.ref_type.unwrap() == ExprType::Double + || span.iter.ref_type.unwrap() == ExprType::Integer ); self.exit_for_level.1 += 1; - if span.iter_double && span.iter.ref_type().is_none() { - let key = SymbolKey::from(span.iter.name()); + if span.iter_double && span.iter.ref_type.is_none() { + let key = SymbolKey::from(&span.iter.name); let skip_pc = self.emit(Instruction::Nop); - let iter_key = SymbolKey::from(span.iter.name()); if self.symtable.get(&key).is_none() { self.emit(Instruction::Dim(DimISpan { - name: key, + name: key.clone(), shared: false, vtype: ExprType::Double, })); - self.symtable.insert(iter_key.clone(), SymbolPrototype::Variable(ExprType::Double)); + self.symtable.insert(key.clone(), SymbolPrototype::Variable(ExprType::Double)); } self.instrs[skip_pc] = Instruction::JumpIfDefined(JumpIfDefinedISpan { - var: iter_key, + var: key, addr: self.instrs.len(), }); } @@ -834,7 +833,7 @@ impl Compiler { self.instrs[end_pc] = Instruction::Jump(JumpISpan { addr: self.instrs.len() }); } - let test_key = SymbolKey::from(test_vref.name()); + let test_key = SymbolKey::from(test_vref.name); self.emit(Instruction::Unset(UnsetISpan { name: test_key.clone(), pos: span.end_pos })); self.symtable.remove(test_key); @@ -894,7 +893,7 @@ impl Compiler { } Statement::Call(span) => { - let key = SymbolKey::from(&span.vref.name()); + let key = SymbolKey::from(&span.vref.name); let (md, upcall_index) = match self.symtable.get(&key) { Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { if md.is_function() { @@ -1112,9 +1111,9 @@ impl Compiler { for span in callable_spans { let pc = self.instrs.len(); - let key = SymbolKey::from(span.name.name()); + let key = SymbolKey::from(span.name.name); let return_value = Compiler::return_key(&key); - match span.name.ref_type() { + match span.name.ref_type { Some(return_type) => { self.emit(Instruction::EnterScope); self.symtable.enter_scope(); @@ -1128,8 +1127,8 @@ impl Compiler { .insert(return_value.clone(), SymbolPrototype::Variable(return_type)); for param in span.params { - let key = SymbolKey::from(param.name()); - let ptype = param.ref_type().unwrap_or(ExprType::Integer); + let key = SymbolKey::from(param.name); + let ptype = param.ref_type.unwrap_or(ExprType::Integer); self.emit(Instruction::Assign(key.clone())); self.symtable.insert(key, SymbolPrototype::Variable(ptype)); } @@ -1162,8 +1161,8 @@ impl Compiler { self.symtable.enter_scope(); for param in span.params { - let key = SymbolKey::from(param.name()); - let ptype = param.ref_type().unwrap_or(ExprType::Integer); + let key = SymbolKey::from(param.name); + let ptype = param.ref_type.unwrap_or(ExprType::Integer); self.emit(Instruction::Assign(key.clone())); self.symtable.insert(key, SymbolPrototype::Variable(ptype)); } @@ -1240,12 +1239,27 @@ impl Compiler { self.instrs[pc] = new_instr; } - let image = - Image { upcalls: self.symtable.upcalls(), instrs: self.instrs, data: self.data }; + let data = compile_data(self.data); + + let image = Image { upcalls: self.symtable.upcalls(), instrs: self.instrs, data }; Ok((image, self.symtable)) } } +/// Translates the reduced set of expressions that represent data into values. +fn compile_data(data: Vec>) -> Vec> { + data.into_iter() + .map(|expr| match expr { + None => None, + Some(Expr::Boolean(span)) => Some(Value::Boolean(span.value)), + Some(Expr::Double(span)) => Some(Value::Double(span.value)), + Some(Expr::Integer(span)) => Some(Value::Integer(span.value)), + Some(Expr::Text(span)) => Some(Value::Text(span.value)), + _ => unreachable!("Valid data types enforced at parse time"), + }) + .collect() +} + /// Compiles a collection of statements into an image ready for execution. /// /// `symtable` is the symbols table as used by the compiler and should be prepopulated with any diff --git a/core/src/exec.rs b/core/src/exec.rs index cd4d6067..a7830c74 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -327,26 +327,26 @@ impl Stack { /// Pops the top of the stack as a variable reference. #[allow(unused)] - fn pop_varref(&mut self) -> (SymbolKey, ExprType) { - let (key, etype, _pos) = self.pop_varref_with_pos(); - (key, etype) + fn pop_varref(&mut self) -> (String, ExprType) { + let (name, etype, _pos) = self.pop_varref_with_pos(); + (name, etype) } /// Pops the top of the stack as a variable reference. // // TODO(jmmv): Remove this variant once the stack values do not carry position // information any longer. - fn pop_varref_with_pos(&mut self) -> (SymbolKey, ExprType, LineCol) { + fn pop_varref_with_pos(&mut self) -> (String, ExprType, LineCol) { match self.values.pop() { - Some((Value::VarRef(key, etype), pos)) => (key, etype, pos), + Some((Value::VarRef(name, etype), pos)) => (name, etype, pos), Some((_, _)) => panic!("Type mismatch"), _ => panic!("Not enough arguments to pop"), } } /// Pushes a variable reference onto the stack. - fn push_varref(&mut self, key: SymbolKey, etype: ExprType, pos: LineCol) { - self.values.push((Value::VarRef(key, etype), pos)); + fn push_varref(&mut self, name: String, etype: ExprType, pos: LineCol) { + self.values.push((Value::VarRef(name, etype), pos)); } /// Peeks into the top of the stack. @@ -491,16 +491,16 @@ impl<'s> Scope<'s> { } /// Pops the top of the stack as a variable reference. - pub fn pop_varref(&mut self) -> (SymbolKey, ExprType) { - let (key, etype, _pos) = self.pop_varref_with_pos(); - (key, etype) + pub fn pop_varref(&mut self) -> (String, ExprType) { + let (name, etype, _pos) = self.pop_varref_with_pos(); + (name, etype) } /// Pops the top of the stack as a variable reference. // // TODO(jmmv): Remove this variant once the stack values do not carry position // information any longer. - pub fn pop_varref_with_pos(&mut self) -> (SymbolKey, ExprType, LineCol) { + pub fn pop_varref_with_pos(&mut self) -> (String, ExprType, LineCol) { debug_assert!(self.nargs > 0, "Not enough arguments in scope"); self.nargs -= 1; self.stack.pop_varref_with_pos() @@ -1413,7 +1413,7 @@ impl Machine { } Instruction::LoadRef(key, etype, pos) => { - context.value_stack.push_varref(key.clone(), *etype, *pos); + context.value_stack.push_varref(key.to_string(), *etype, *pos); context.pc += 1; } @@ -1630,9 +1630,9 @@ mod tests { (Value::Double(1.2), LineCol { line: 1, col: 2 }), (Value::Integer(2), LineCol { line: 1, col: 2 }), (Value::Text("foo".to_owned()), LineCol { line: 1, col: 2 }), - (Value::VarRef(SymbolKey::from("foo"), ExprType::Integer), LineCol { line: 1, col: 2 }), + (Value::VarRef("FOO".to_owned(), ExprType::Integer), LineCol { line: 1, col: 2 }), ]); - assert_eq!((SymbolKey::from("foo"), ExprType::Integer), stack.pop_varref()); + assert_eq!(("FOO".to_owned(), ExprType::Integer), stack.pop_varref()); assert_eq!("foo", stack.pop_string()); assert_eq!(2, stack.pop_integer()); assert_eq!(1.2, stack.pop_double()); @@ -1646,13 +1646,10 @@ mod tests { (Value::Double(1.2), LineCol { line: 3, col: 4 }), (Value::Integer(2), LineCol { line: 5, col: 6 }), (Value::Text("foo".to_owned()), LineCol { line: 7, col: 8 }), - ( - Value::VarRef(SymbolKey::from("foo"), ExprType::Integer), - LineCol { line: 9, col: 10 }, - ), + (Value::VarRef("FOO".to_owned(), ExprType::Integer), LineCol { line: 9, col: 10 }), ]); assert_eq!( - (SymbolKey::from("foo"), ExprType::Integer, LineCol { line: 9, col: 10 }), + ("FOO".to_owned(), ExprType::Integer, LineCol { line: 9, col: 10 }), stack.pop_varref_with_pos() ); assert_eq!(("foo".to_owned(), LineCol { line: 7, col: 8 }), stack.pop_string_with_pos()); @@ -1668,14 +1665,14 @@ mod tests { stack.push_double(1.2, LineCol { line: 1, col: 2 }); stack.push_integer(2, LineCol { line: 1, col: 2 }); stack.push_string("foo".to_owned(), LineCol { line: 1, col: 2 }); - stack.push_varref(SymbolKey::from("foo"), ExprType::Integer, LineCol { line: 1, col: 2 }); + stack.push_varref("FOO".to_owned(), ExprType::Integer, LineCol { line: 1, col: 2 }); let exp_values = vec![ (Value::Boolean(false), LineCol { line: 1, col: 2 }), (Value::Double(1.2), LineCol { line: 1, col: 2 }), (Value::Integer(2), LineCol { line: 1, col: 2 }), (Value::Text("foo".to_owned()), LineCol { line: 1, col: 2 }), - (Value::VarRef(SymbolKey::from("foo"), ExprType::Integer), LineCol { line: 1, col: 2 }), + (Value::VarRef("FOO".to_owned(), ExprType::Integer), LineCol { line: 1, col: 2 }), ]; assert_eq!(exp_values, stack.values); } @@ -1722,10 +1719,10 @@ mod tests { (Value::Double(1.2), LineCol { line: 1, col: 2 }), (Value::Integer(2), LineCol { line: 1, col: 2 }), (Value::Text("foo".to_owned()), LineCol { line: 1, col: 2 }), - (Value::VarRef(SymbolKey::from("foo"), ExprType::Integer), LineCol { line: 1, col: 2 }), + (Value::VarRef("FOO".to_owned(), ExprType::Integer), LineCol { line: 1, col: 2 }), ]); let mut scope = Scope::new(&mut stack, 5, LineCol { line: 50, col: 60 }); - assert_eq!((SymbolKey::from("foo"), ExprType::Integer), scope.pop_varref()); + assert_eq!(("FOO".to_owned(), ExprType::Integer), scope.pop_varref()); assert_eq!("foo", scope.pop_string()); assert_eq!(2, scope.pop_integer()); assert_eq!(1.2, scope.pop_double()); @@ -1739,14 +1736,11 @@ mod tests { (Value::Double(1.2), LineCol { line: 3, col: 4 }), (Value::Integer(2), LineCol { line: 5, col: 6 }), (Value::Text("foo".to_owned()), LineCol { line: 7, col: 8 }), - ( - Value::VarRef(SymbolKey::from("foo"), ExprType::Integer), - LineCol { line: 9, col: 10 }, - ), + (Value::VarRef("FOO".to_owned(), ExprType::Integer), LineCol { line: 9, col: 10 }), ]); let mut scope = Scope::new(&mut stack, 5, LineCol { line: 50, col: 60 }); assert_eq!( - (SymbolKey::from("foo"), ExprType::Integer, LineCol { line: 9, col: 10 }), + ("FOO".to_owned(), ExprType::Integer, LineCol { line: 9, col: 10 }), scope.pop_varref_with_pos() ); assert_eq!(("foo".to_owned(), LineCol { line: 7, col: 8 }), scope.pop_string_with_pos()); diff --git a/core/src/lexer.rs b/core/src/lexer.rs index e358f612..ce8130c5 100644 --- a/core/src/lexer.rs +++ b/core/src/lexer.rs @@ -639,7 +639,7 @@ impl<'a> Lexer<'a> { return Ok(TokenSpan::new(Token::Eof, last_pos, 0)); } Some(Ok(ch_span)) if ch_span.ch == '\n' => { - return Ok(TokenSpan::new(Token::Eol, ch_span.pos, 1)) + return Ok(TokenSpan::new(Token::Eol, ch_span.pos, 1)); } Some(Err(e)) => return Err(e), Some(Ok(_)) => (), diff --git a/core/src/lib.rs b/core/src/lib.rs index e6bb17be..bca4db0f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -15,13 +15,6 @@ //! The EndBASIC language parser and interpreter. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - // TODO(jmmv): Should narrow the exposed interface by 1.0.0. pub mod ast; pub mod bytecode; diff --git a/core/src/parser.rs b/core/src/parser.rs index 96129ed6..d72a6e4d 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -46,10 +46,10 @@ pub type Result = std::result::Result; /// /// This is only valid for references that have no annotations in them. fn vref_to_unannotated_string(vref: VarRef, pos: LineCol) -> Result { - if vref.ref_type().is_some() { + if vref.ref_type.is_some() { return Err(Error::Bad(pos, format!("Type annotation not allowed in {}", vref))); } - Ok(vref.take_name()) + Ok(vref.name) } /// Converts a collection of `ArgSpan`s passed to a function or array reference to a collection @@ -364,7 +364,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( peeked.pos, "Expected comma, semicolon, or end of statement".to_owned(), - )) + )); } } } @@ -436,21 +436,35 @@ impl<'a> Parser<'a> { let token_span = self.lexer.read()?; match token_span.token { - Token::Boolean(b) => values.push(Some(Value::Boolean(b))), - Token::Double(d) => values.push(Some(Value::Double(d))), - Token::Integer(i) => values.push(Some(Value::Integer(i))), - Token::Text(t) => values.push(Some(Value::Text(t))), + Token::Boolean(b) => { + values.push(Some(Expr::Boolean(BooleanSpan { value: b, pos: token_span.pos }))) + } + Token::Double(d) => { + values.push(Some(Expr::Double(DoubleSpan { value: d, pos: token_span.pos }))) + } + Token::Integer(i) => { + values.push(Some(Expr::Integer(IntegerSpan { value: i, pos: token_span.pos }))) + } + Token::Text(t) => { + values.push(Some(Expr::Text(TextSpan { value: t, pos: token_span.pos }))) + } Token::Minus => { - let token_span = self.lexer.read()?; - match token_span.token { - Token::Double(d) => values.push(Some(Value::Double(-d))), - Token::Integer(i) => values.push(Some(Value::Integer(-i))), + let token_span2 = self.lexer.read()?; + match token_span2.token { + Token::Double(d) => values.push(Some(Expr::Double(DoubleSpan { + value: -d, + pos: token_span.pos, + }))), + Token::Integer(i) => values.push(Some(Expr::Integer(IntegerSpan { + value: -i, + pos: token_span.pos, + }))), _ => { return Err(Error::Bad( token_span.pos, "Expected number after -".to_owned(), - )) + )); } } } @@ -468,7 +482,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( token_span.pos, format!("Unexpected {} in DATA statement", t), - )) + )); } } @@ -486,7 +500,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( peeked.pos, format!("Expected comma after datum but found {}", t), - )) + )); } } } @@ -531,7 +545,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( token_span.pos, "Expected variable name after DIM".to_owned(), - )) + )); } }; let name = vref_to_unannotated_string(vref, token_span.pos)?; @@ -614,7 +628,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( do_pos, "DO loop cannot have pre and post guards at the same time".to_owned(), - )) + )); } }; @@ -681,7 +695,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( peeked.pos, "Expecting DO, FOR, FUNCTION or SUB after EXIT".to_owned(), - )) + )); } }; self.lexer.consume_peeked(); @@ -964,17 +978,13 @@ impl<'a> Parser<'a> { while let Some(eos) = op_spans.pop() { match eos.op { ExprOp::LeftParen => { - return Err(Error::Bad(eos.pos, "Unbalanced parenthesis".to_owned())) + return Err(Error::Bad(eos.pos, "Unbalanced parenthesis".to_owned())); } _ => eos.apply(&mut exprs)?, } } - if let Some(expr) = exprs.pop() { - Ok(Some(expr)) - } else { - Ok(None) - } + if let Some(expr) = exprs.pop() { Ok(Some(expr)) } else { Ok(None) } } /// Wrapper over `parse_expr` that requires an expression to be present and returns an error @@ -1221,20 +1231,20 @@ impl<'a> Parser<'a> { fn parse_for(&mut self, for_pos: LineCol) -> Result { let token_span = self.lexer.read()?; let iterator = match token_span.token { - Token::Symbol(iterator) => match iterator.ref_type() { + Token::Symbol(iterator) => match iterator.ref_type { None | Some(ExprType::Double) | Some(ExprType::Integer) => iterator, _ => { return Err(Error::Bad( token_span.pos, "Iterator name in FOR statement must be a numeric reference".to_owned(), - )) + )); } }, _ => { return Err(Error::Bad( token_span.pos, "No iterator name in FOR statement".to_owned(), - )) + )); } }; let iterator_pos = token_span.pos; @@ -1261,7 +1271,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( step.start_pos(), "Infinite FOR loop; STEP cannot be 0".to_owned(), - )) + )); } }; @@ -1426,8 +1436,8 @@ impl<'a> Parser<'a> { let token_span = self.lexer.read()?; let name = match token_span.token { Token::Symbol(name) => { - if name.ref_type().is_none() { - VarRef::new(name.take_name(), Some(ExprType::Integer)) + if name.ref_type.is_none() { + VarRef::new(name.name, Some(ExprType::Integer)) } else { name } @@ -1454,7 +1464,7 @@ impl<'a> Parser<'a> { let token_span = self.lexer.read()?; let name = match token_span.token { Token::Symbol(name) => { - if name.ref_type().is_some() { + if name.ref_type.is_some() { return Err(Error::Bad( token_span.pos, "SUBs cannot return a value so type annotations are not allowed".to_owned(), @@ -1931,7 +1941,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( token_span.pos, format!("Expected newline but found {}", token_span.token), - )) + )); } }; @@ -2377,8 +2387,12 @@ mod tests { do_ok_test( "DATA 1: DATA 2", &[ - Statement::Data(DataSpan { values: vec![Some(Value::Integer(1))] }), - Statement::Data(DataSpan { values: vec![Some(Value::Integer(2))] }), + Statement::Data(DataSpan { + values: vec![Some(Expr::Integer(IntegerSpan { value: 1, pos: lc(1, 6) }))], + }), + Statement::Data(DataSpan { + values: vec![Some(Expr::Integer(IntegerSpan { value: 2, pos: lc(1, 14) }))], + }), ], ); @@ -2386,10 +2400,10 @@ mod tests { "DATA TRUE, -3, 5.1, \"foo\"", &[Statement::Data(DataSpan { values: vec![ - Some(Value::Boolean(true)), - Some(Value::Integer(-3)), - Some(Value::Double(5.1)), - Some(Value::Text("foo".to_owned())), + Some(Expr::Boolean(BooleanSpan { value: true, pos: lc(1, 6) })), + Some(Expr::Integer(IntegerSpan { value: -3, pos: lc(1, 12) })), + Some(Expr::Double(DoubleSpan { value: 5.1, pos: lc(1, 16) })), + Some(Expr::Text(TextSpan { value: "foo".to_owned(), pos: lc(1, 21) })), ], })], ); @@ -2399,13 +2413,13 @@ mod tests { &[Statement::Data(DataSpan { values: vec![ None, - Some(Value::Boolean(true)), + Some(Expr::Boolean(BooleanSpan { value: true, pos: lc(1, 8) })), None, - Some(Value::Integer(3)), + Some(Expr::Integer(IntegerSpan { value: 3, pos: lc(1, 16) })), None, - Some(Value::Double(5.1)), + Some(Expr::Double(DoubleSpan { value: 5.1, pos: lc(1, 21) })), None, - Some(Value::Text("foo".to_owned())), + Some(Expr::Text(TextSpan { value: "foo".to_owned(), pos: lc(1, 28) })), None, ], })], @@ -2414,7 +2428,10 @@ mod tests { do_ok_test( "DATA -3, -5.1", &[Statement::Data(DataSpan { - values: vec![Some(Value::Integer(-3)), Some(Value::Double(-5.1))], + values: vec![ + Some(Expr::Integer(IntegerSpan { value: -3, pos: lc(1, 6) })), + Some(Expr::Double(DoubleSpan { value: -5.1, pos: lc(1, 10) })), + ], })], ); } @@ -2425,9 +2442,9 @@ mod tests { do_error_test("DATA ;", "1:6: Unexpected ; in DATA statement"); do_error_test("DATA 5 + 1", "1:8: Expected comma after datum but found +"); do_error_test("DATA 5 ; 1", "1:8: Expected comma after datum but found ;"); - do_error_test("DATA -FALSE", "1:7: Expected number after -"); - do_error_test("DATA -\"abc\"", "1:7: Expected number after -"); - do_error_test("DATA -foo", "1:7: Expected number after -"); + do_error_test("DATA -FALSE", "1:6: Expected number after -"); + do_error_test("DATA -\"abc\"", "1:6: Expected number after -"); + do_error_test("DATA -foo", "1:6: Expected number after -"); } #[test] diff --git a/core/src/syms.rs b/core/src/syms.rs index b3c436ab..51c6d1c1 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -353,7 +353,7 @@ impl Symbols { /// /// Returns an error if the type annotation in the symbol reference does not match its type. pub fn get(&self, vref: &VarRef) -> value::Result> { - let key = SymbolKey::from(vref.name()); + let key = SymbolKey::from(&vref.name); let symbol = self.load(&key); if let Some(symbol) = symbol { let stype = symbol.eval_type(); @@ -377,7 +377,7 @@ impl Symbols { /// /// Returns an error if the type annotation in the symbol reference does not match its type. pub fn get_mut(&mut self, vref: &VarRef) -> value::Result> { - match self.load_mut(&vref.as_symbol_key()) { + match self.load_mut(&SymbolKey::from(&vref.name)) { Some(symbol) => { let stype = symbol.eval_type(); if !vref.accepts_callable(stype) { @@ -401,8 +401,8 @@ impl Symbols { pub(crate) fn get_var(&self, vref: &VarRef) -> value::Result<&Value> { match self.get(vref)? { Some(Symbol::Variable(v)) => Ok(v), - Some(_) => Err(value::Error::new(format!("{} is not a variable", vref.name()))), - None => Err(value::Error::new(format!("Undefined variable {}", vref.name()))), + Some(_) => Err(value::Error::new(format!("{} is not a variable", vref.name))), + None => Err(value::Error::new(format!("Undefined variable {}", vref.name))), } } @@ -440,8 +440,8 @@ impl Symbols { /// If the variable is already defined, then the type of the new value must be compatible with /// the existing variable. In other words: a variable cannot change types while it's alive. pub fn set_var(&mut self, vref: &VarRef, value: Value) -> value::Result<()> { - let key = vref.as_symbol_key(); - let value = value.maybe_cast(vref.ref_type())?; + let key = SymbolKey::from(&vref.name); + let value = value.maybe_cast(vref.ref_type)?; match self.get_mut(vref)? { Some(Symbol::Variable(old_value)) => { let value = value.maybe_cast(Some(old_value.as_exprtype()))?; @@ -457,14 +457,14 @@ impl Symbols { } Some(_) => Err(value::Error::new(format!("Cannot redefine {} as a variable", vref))), None => { - if let Some(ref_type) = vref.ref_type() { - if !vref.accepts(value.as_exprtype()) { - return Err(value::Error::new(format!( - "Cannot assign value of type {} to variable of type {}", - value.as_exprtype(), - ref_type, - ))); - } + if let Some(ref_type) = vref.ref_type + && !vref.accepts(value.as_exprtype()) + { + return Err(value::Error::new(format!( + "Cannot assign value of type {} to variable of type {}", + value.as_exprtype(), + ref_type, + ))); } self.scopes.last_mut().unwrap().insert(key, Symbol::Variable(value)); Ok(()) @@ -1119,10 +1119,9 @@ mod tests { fn test_symbols_get_mut_undefined() { // If modifying this test, update the identical test for get() and get_auto(). let mut syms = SymbolsBuilder::default().add_var("SOMETHING", Value::Integer(3)).build(); - assert!(syms - .get_mut(&VarRef::new("SOME_THIN", Some(ExprType::Integer))) - .unwrap() - .is_none()); + assert!( + syms.get_mut(&VarRef::new("SOME_THIN", Some(ExprType::Integer))).unwrap().is_none() + ); } #[test] diff --git a/core/src/testutils.rs b/core/src/testutils.rs index d15b1a2d..f8ef7f12 100644 --- a/core/src/testutils.rs +++ b/core/src/testutils.rs @@ -315,7 +315,7 @@ impl Callable for InCommand { ExprType::Text => Value::Text(raw_value.to_string()), _ => unreachable!("Unsupported target type"), }; - machine.get_mut_symbols().assign(&vname, value); + machine.get_mut_symbols().assign(&SymbolKey::from(vname), value); Ok(()) } } diff --git a/core/src/value.rs b/core/src/value.rs index 2c3c6a97..d4e3dcda 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -72,7 +72,7 @@ pub(crate) fn bitwise_shl(lhs: i32, rhs: i32) -> Result { let bits = match u32::try_from(rhs) { Ok(n) => n, Err(_) => { - return Err(Error::new(format!("Number of bits to << ({}) must be positive", rhs))) + return Err(Error::new(format!("Number of bits to << ({}) must be positive", rhs))); } }; @@ -87,7 +87,7 @@ pub(crate) fn bitwise_shr(lhs: i32, rhs: i32) -> Result { let bits = match u32::try_from(rhs) { Ok(n) => n, Err(_) => { - return Err(Error::new(format!("Number of bits to >> ({}) must be positive", rhs))) + return Err(Error::new(format!("Number of bits to >> ({}) must be positive", rhs))); } }; diff --git a/core/tests/integration_test.rs b/core/tests/integration_test.rs index ede6d602..c921d185 100644 --- a/core/tests/integration_test.rs +++ b/core/tests/integration_test.rs @@ -15,11 +15,6 @@ //! Integration tests that use golden input and output files. -// Keep these in sync with other top-level files. -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use std::env; use std::fs::File; use std::io::Read; @@ -71,11 +66,7 @@ fn read_golden(path: &Path) -> String { let mut golden = vec![]; f.read_to_end(&mut golden).expect("Failed to read golden data file"); let raw = String::from_utf8(golden).expect("Golden data file is not valid UTF-8"); - if cfg!(target_os = "windows") { - raw.replace("\r\n", "\n") - } else { - raw - } + if cfg!(target_os = "windows") { raw.replace("\r\n", "\n") } else { raw } } /// Runs `bin` with arguments `args` and checks its behavior against expectations. diff --git a/core2/Cargo.toml b/core2/Cargo.toml new file mode 100644 index 00000000..74c39f1e --- /dev/null +++ b/core2/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "endbasic-core2" +version = "0.11.99" # ENDBASIC-VERSION +license = "Apache-2.0" +authors = ["Julio Merino "] +categories = ["development-tools", "parser-implementations"] +keywords = ["basic", "interpreter", "learning", "programming"] +description = "The EndBASIC programming language - core" +homepage = "https://www.endbasic.dev/" +repository = "https://github.com/endbasic/endbasic" +readme = "README.md" +edition = "2018" +publish = false + +[dependencies] +async-channel = "2.2" +async-trait = "0.1" +thiserror = "1.0" + +[dev-dependencies] +futures-lite = "2.2" +tokio = { version = "1", features = ["full"] } diff --git a/core2/LICENSE b/core2/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/core2/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/core2/NOTICE b/core2/NOTICE new file mode 100644 index 00000000..a6ebc462 --- /dev/null +++ b/core2/NOTICE @@ -0,0 +1,2 @@ +EndBASIC +Copyright 2020-2026 Julio Merino diff --git a/core2/README.md b/core2/README.md new file mode 100644 index 00000000..c0c68c99 --- /dev/null +++ b/core2/README.md @@ -0,0 +1,85 @@ +# The EndBASIC programming language - core + +[![Crates.io](https://img.shields.io/crates/v/endbasic-core.svg)](https://crates.io/crates/endbasic-core/) +[![Docs.rs](https://docs.rs/endbasic-core/badge.svg)](https://docs.rs/endbasic-core/) + +EndBASIC is an interpreter for a BASIC-like language and is inspired by +Amstrad's Locomotive BASIC 1.1 and Microsoft's QuickBASIC 4.5. Like the former, +EndBASIC intends to provide an interactive environment that seamlessly merges +coding with immediate visual feedback. Like the latter, EndBASIC offers +higher-level programming constructs and strong typing. + +EndBASIC offers a simplified and restricted environment to learn the foundations +of programming and focuses on features that can quickly reward the programmer. +These features include things like a built-in text editor, commands to +render graphics, and commands to interact with the hardware of a Raspberry +Pi. Implementing this kind of features has priority over others such as +performance or a much richer language. + +EndBASIC is written in Rust and runs both on the web and locally on a variety of +operating systems and platforms, including macOS, Windows, and Linux. + +EndBASIC is free software under the [Apache 2.0 License](LICENSE). + +## What's in this crate? + +`endbasic-core` provides the language parser and interpreter. By design, this +crate provides zero commands and zero functions. + +## Language features + +EndBASIC's language features are inspired by other BASIC interpreters but the +language does not intend to be fully compatible with them. The language +currently supports: + +* Variable types: boolean (`?`), double (`#`), integer (`%`), and string + (`$`). +* Arrays via `DIM name(1, 2, 3) AS type`. +* Strong typing with optional variable type annotations. +* `DATA` statements for literal primitive values. Booleans, numbers, and + strings are supported, but strings must be double-quoted. +* `DO` / `LOOP` statements with optional `UNTIL` / `WHILE` pre- and + post-guards and optional `EXIT DO` early terminations. +* `DIM SHARED` for global variables. +* `FUNCTION name` / `EXIT FUNCTION` / `END FUNCTION`. +* `IF ... THEN ... [ELSE ...]` uniline statements. +* `IF ... THEN` / `ELSEIF ... THEN` / `ELSE` / `END IF` multiline + statements. +* `FOR x = ... TO ... [STEP ...]` / `NEXT` loops with optional `EXIT FOR` + early terminations. +* `GOSUB line` / `GOSUB @label` / `RETURN` for procedure execution. +* `GOTO line` / `GOTO @label` statements and `@label` annotations. +* `SELECT CASE` / `CASE ...` / `CASE IS ...` / `CASE ... TO ...` / + `END SELECT` statements. +* `SUB name` / `EXIT SUB` / `END SUB`. +* `WHILE ...` / `WEND` loops. +* Error handling via `ON ERROR GOTO` and `ON ERROR RESUME NEXT`. +* UTF-8 everywhere (I think). + +## Design principles + +Some highlights about the EndBASIC implementation are: + +* Minimalist core. The interpreter knows how to execute the logic of the + language but, by default, it exposes no builtins to the scripts—not even + `INPUT` or `PRINT`. This makes EndBASIC ideal for embedding into other + programs, as it is possible to execute external code without side-effects or + by precisely controlling how such code interacts with the host program. + +* Async support. The interpreter is async-compatible, making it trivial to + embed it into Javascript via WASM. + +## Examples + +The `examples` directory contains sample code to show how to embed the EndBASIC +interpreter into your own programs. In particular: + +* [`examples/config.rs`](examples/config.rs): Shows how to instantiate a + minimal EndBASIC interpreter and uses it to implement what could be a + configuration file parser. + +* [`examples/dsl.rs`](example/dsl.rs): Shows how to instantiate an EndBASIC + interpreter with custom functions and commands to construct what could be a + domain-specific language. This language is then used to control some + hypothetical hardware lights and exemplifies how to bridge the Rust world + and the EndBASIC world. diff --git a/core2/examples/config.rs b/core2/examples/config.rs new file mode 100644 index 00000000..0666a141 --- /dev/null +++ b/core2/examples/config.rs @@ -0,0 +1,119 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Configuration file parser using an EndBASIC interpreter. +//! +//! This example sets up a minimal EndBASIC interpreter and uses it to parse what could be a +//! configuration file. Because the interpreter is configured without any commands or functions, +//! the scripted code cannot call back into Rust land, so the script's execution is guaranteed to +//! not have side-effects. + +use std::collections::HashMap; +use std::rc::Rc; + +use async_trait::async_trait; +use endbasic_core2::*; +use futures_lite::future::block_on; + +/// Sample configuration file to parse. +const INPUT: &str = r#" +foo_value = 123 +bar_value = 2147483640 +TEST 10 + 2 +foo_value = 123 +TEST 212 +'Enable_bar = (foo_value > 122) +'enable_baz = "this is commented out" +"#; + +struct TestCommand { + metadata: CallableMetadata, +} + +impl TestCommand { + pub fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("TEST") + .with_syntax(&[(&[], None)]) + .with_category("Demonstration") + .with_description("Turns the light identified by 'id' on or off.") + .build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for TestCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + //async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + async fn exec(&self, scope: Scope<'_>) { + eprintln!("TEST with arg {}", scope.get_integer(0)); + } +} + +fn main() { + let mut upcalls_by_name: HashMap> = HashMap::default(); + upcalls_by_name.insert(SymbolKey::from("TEST"), TestCommand::new()); + let image = compile(&mut INPUT.as_bytes(), &upcalls_by_name).unwrap(); + let upcalls = image.map_upcalls(&upcalls_by_name); + + let mut vm = Vm::new(upcalls_by_name); + let mut context = vm.load(image); + loop { + match vm.exec(&mut context) { + StopReason::Eof => break, + StopReason::Upcall(index, nargs) => { + eprintln!("Invoking upcall {} with nargs {}", index, nargs); + block_on(upcalls[index].exec(context.new_scope(nargs))); + } + } + } + + // // Create an empty machine. + // let mut machine = Machine::default(); + // + // // Execute the sample script. All this script can do is modify the state of the machine itself. + // // In other words: the script can set variables in the machine's environment, but that's it. + // loop { + // match block_on(machine.exec(&mut INPUT.as_bytes())).expect("Execution error") { + // StopReason::Eof => break, + // StopReason::Exited(i) => println!("Script explicitly exited with code {}", i), + // StopReason::Break => (), // Ignore signals. + // } + // } + // + // // Now that our script has run, inspect the variables it set on the machine. + // match machine.get_symbols().get_auto("foo_value") { + // Some(Symbol::Variable(Value::Integer(i))) => { + // println!("foo_value is {}", i) + // } + // _ => println!("Input did not contain foo_value or is of an invalid type"), + // } + // match machine.get_symbols().get_auto("enable_bar") { + // Some(Symbol::Variable(Value::Boolean(b))) => { + // println!("enable_bar is {}", b) + // } + // _ => println!("Input did not contain enable_bar or is of an invalid type"), + // } + // match machine.get_symbols().get_auto("enable_baz") { + // Some(_) => { + // println!("enable_baz should not have been set") + // } + // _ => println!("enable_baz is not set"), + // } +} diff --git a/core2/src/ast.rs b/core2/src/ast.rs new file mode 100644 index 00000000..1d444417 --- /dev/null +++ b/core2/src/ast.rs @@ -0,0 +1,783 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Abstract Syntax Tree (AST) for the EndBASIC language. + +use crate::reader::LineCol; +use std::fmt; + +/// Components of a boolean literal expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BooleanSpan { + /// The boolean literal. + pub value: bool, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of a double literal expression. +#[derive(Clone, Debug, PartialEq)] +pub struct DoubleSpan { + /// The double literal. + pub value: f64, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of an integer literal expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IntegerSpan { + /// The integer literal. + pub value: i32, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of a string literal expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TextSpan { + /// The string literal. + pub value: String, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of a symbol reference expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SymbolSpan { + /// The symbol reference. + pub vref: VarRef, + + /// Starting position of the symbol reference. + pub pos: LineCol, +} + +/// Components of a unary operation expression. +#[derive(Clone, Debug, PartialEq)] +pub struct UnaryOpSpan { + /// Expression affected by the operator. + pub expr: Expr, + + /// Starting position of the operator. + pub pos: LineCol, +} + +/// Components of a binary operation expression. +#[derive(Clone, Debug, PartialEq)] +pub struct BinaryOpSpan { + /// Expression on the left side of the operator. + pub lhs: Expr, + + /// Expression on the right side of the operator. + pub rhs: Expr, + + /// Starting position of the operator. + pub pos: LineCol, +} + +/// Represents an expression and provides mechanisms to evaluate it. +#[derive(Clone, Debug, PartialEq)] +pub enum Expr { + /// A literal boolean value. + Boolean(BooleanSpan), + /// A literal double-precision floating point value. + Double(DoubleSpan), + /// A literal integer value. + Integer(IntegerSpan), + /// A literal string value. + Text(TextSpan), + + /// A reference to a variable. + Symbol(SymbolSpan), + + /// Arithmetic addition of two expressions. + Add(Box), + /// Arithmetic subtraction of two expressions. + Subtract(Box), + /// Arithmetic multiplication of two expressions. + Multiply(Box), + /// Arithmetic division of two expressions. + Divide(Box), + /// Arithmetic modulo operation of two expressions. + Modulo(Box), + /// Arithmetic power operation of two expressions. + Power(Box), + /// Arithmetic sign flip of an expression. + Negate(Box), + + /// Relational equality comparison of two expressions. + Equal(Box), + /// Relational inequality comparison of two expressions. + NotEqual(Box), + /// Relational less-than comparison of two expressions. + Less(Box), + /// Relational less-than or equal-to comparison of two expressions. + LessEqual(Box), + /// Relational greater-than comparison of two expressions. + Greater(Box), + /// Relational greater-than or equal-to comparison of two expressions. + GreaterEqual(Box), + + /// Logical and of two expressions. + And(Box), + /// Logical not of an expression. + Not(Box), + /// Logical or of two expressions. + Or(Box), + /// Logical xor of two expressions. + Xor(Box), + + /// Shift left of a signed integer by a number of bits without rotation. + ShiftLeft(Box), + /// Shift right of a signed integer by a number of bits without rotation. + ShiftRight(Box), + + /// A function call or an array reference. + Call(CallSpan), +} + +impl Expr { + /// Returns the start position of the expression. + pub fn start_pos(&self) -> LineCol { + match self { + Expr::Boolean(span) => span.pos, + Expr::Double(span) => span.pos, + Expr::Integer(span) => span.pos, + Expr::Text(span) => span.pos, + + Expr::Symbol(span) => span.pos, + + Expr::And(span) => span.lhs.start_pos(), + Expr::Or(span) => span.lhs.start_pos(), + Expr::Xor(span) => span.lhs.start_pos(), + Expr::Not(span) => span.pos, + + Expr::ShiftLeft(span) => span.lhs.start_pos(), + Expr::ShiftRight(span) => span.lhs.start_pos(), + + Expr::Equal(span) => span.lhs.start_pos(), + Expr::NotEqual(span) => span.lhs.start_pos(), + Expr::Less(span) => span.lhs.start_pos(), + Expr::LessEqual(span) => span.lhs.start_pos(), + Expr::Greater(span) => span.lhs.start_pos(), + Expr::GreaterEqual(span) => span.lhs.start_pos(), + + Expr::Add(span) => span.lhs.start_pos(), + Expr::Subtract(span) => span.lhs.start_pos(), + Expr::Multiply(span) => span.lhs.start_pos(), + Expr::Divide(span) => span.lhs.start_pos(), + Expr::Modulo(span) => span.lhs.start_pos(), + Expr::Power(span) => span.lhs.start_pos(), + Expr::Negate(span) => span.pos, + + Expr::Call(span) => span.vref_pos, + } + } +} + +/// Represents type of an expression. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ExprType { + /// Type for an expression that evaluates to a boolean. + Boolean, + + /// Type for an expression that evaluates to a double. + Double, + + /// Type for an expression that evaluates to an integer. + Integer, + + /// Type for an expression that evaluates to a string. + Text, +} + +impl ExprType { + /// Returns true if this expression type is numerical. + pub(crate) fn is_numerical(self) -> bool { + self == Self::Double || self == Self::Integer + } + + /// Returns the textual representation of the annotation for this type. + pub fn annotation(&self) -> char { + match self { + ExprType::Boolean => '?', + ExprType::Double => '#', + ExprType::Integer => '%', + ExprType::Text => '$', + } + } +} + +impl fmt::Display for ExprType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExprType::Boolean => write!(f, "BOOLEAN"), + ExprType::Double => write!(f, "DOUBLE"), + ExprType::Integer => write!(f, "INTEGER"), + ExprType::Text => write!(f, "STRING"), + } + } +} + +/// Represents a reference to a variable (which doesn't have to exist). +/// +/// Variable references are different from `SymbolKey`s because they maintain the case of the +/// reference (for error display purposes) and because they carry an optional type annotation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VarRef { + /// Name of the variable this points to. + pub name: String, + + /// Type of the variable this points to, if explicitly specified. + /// + /// If `None`, the type of the variable is subject to type inference. + pub ref_type: Option, +} + +impl VarRef { + /// Creates a new reference to the variable with `name` and the optional `ref_type` type. + pub fn new>(name: T, ref_type: Option) -> Self { + Self { name: name.into(), ref_type } + } + + /// Returns true if this reference is compatible with the given type. + pub fn accepts(&self, other: ExprType) -> bool { + match self.ref_type { + None => true, + Some(vtype) => vtype == other, + } + } + + /// Returns true if this reference is compatible with the return type of a callable. + pub fn accepts_callable(&self, other: Option) -> bool { + match self.ref_type { + None => true, + Some(vtype) => match other { + Some(other) => vtype == other, + None => false, + }, + } + } +} + +impl fmt::Display for VarRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.ref_type { + None => self.name.fmt(f), + Some(vtype) => write!(f, "{}{}", self.name, vtype.annotation()), + } + } +} + +/// Types of separators between arguments to a `BuiltinCall`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ArgSep { + /// Filler for the separator in the last argument. + End = 0, + + /// Short separator (`;`). + Short = 1, + + /// Long separator (`,`). + Long = 2, + + /// `AS` separator. + As = 3, +} + +impl fmt::Display for ArgSep { + // TODO(jmmv): Can this be removed in favor of describe()? + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ArgSep::End => write!(f, ""), + ArgSep::Short => write!(f, ";"), + ArgSep::Long => write!(f, ","), + ArgSep::As => write!(f, "AS"), + } + } +} + +impl ArgSep { + /// Formats the separator for a syntax specification. + /// + /// The return value contains the textual representation of the separator and a boolean that + /// indicates whether the separator requires a leading space. + pub(crate) fn describe(&self) -> (&str, bool) { + match self { + ArgSep::End => ("", false), + ArgSep::Short => (";", false), + ArgSep::Long => (",", false), + ArgSep::As => ("AS", true), + } + } +} + +/// Components of an array assignment statement. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct ArrayAssignmentSpan { + /// Reference to the array to modify. + pub vref: VarRef, + + /// Position of the `vref`. + pub vref_pos: LineCol, + + /// Expressions to compute the subscripts to index the array. + pub subscripts: Vec, + + /// Expression to compute the value of the modified element. + pub expr: Expr, +} + +/// Components of an assignment statement. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct AssignmentSpan { + /// Reference to the variable to set. + pub vref: VarRef, + + /// Position of the `vref`. + pub vref_pos: LineCol, + + /// Expression to compute the value of the modified variable. + pub expr: Expr, +} + +/// Single argument to a builtin call statement. +#[derive(Clone, Debug, PartialEq)] +pub struct ArgSpan { + /// Expression to compute the argument's value. This expression is optional to support calls + /// of the form `PRINT a, , b` where some arguments are empty. + pub expr: Option, + + /// Separator between this argument and the *next*. The last instance of this type in a call + /// always carries a value of `ArgSep::End`. + pub sep: ArgSep, + + /// Position of the `sep`. + pub sep_pos: LineCol, +} + +/// Components of a call statement or expression. +#[derive(Clone, Debug, PartialEq)] +pub struct CallSpan { + /// Reference to the callable (a command or a function), or the array to index. + pub vref: VarRef, + + /// Position of the reference. + pub vref_pos: LineCol, + + /// Sequence of arguments to pass to the callable. + pub args: Vec, +} + +/// Components of a `FUNCTION` or `SUB` definition. +#[derive(Debug, PartialEq)] +pub struct CallableSpan { + /// Name of the callable, expressed as a variable reference. For functions, this contains + /// a type, and for subroutines, it does not. + pub name: VarRef, + + /// Position of the name of the callable. + pub name_pos: LineCol, + + /// Definition of the callable parameters. + pub params: Vec, + + /// Statements within the callable's body. + pub body: Vec, + + /// Position of the end of the callable, used when injecting the implicit return. + pub end_pos: LineCol, +} + +/// Components of a data statement. +#[derive(Debug, PartialEq)] +pub struct DataSpan { + /// Collection of optional literal values. + pub values: Vec>, +} + +/// Components of a variable definition. +/// +/// Given that a definition causes the variable to be initialized to a default value, it is +/// tempting to model this statement as a simple assignment. However, we must be able to +/// detect variable redeclarations at runtime, so we must treat this statement as a separate +/// type from assignments. +#[derive(Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct DimSpan { + /// Name of the variable to be defined. Type annotations are not allowed, hence why this is + /// not a `VarRef`. + pub name: String, + + /// Position of the name. + pub name_pos: LineCol, + + /// Whether the variable is global or not. + pub shared: bool, + + /// Type of the variable to be defined. + pub vtype: ExprType, + + /// Position of the type. + pub vtype_pos: LineCol, +} + +/// Components of an array definition. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct DimArraySpan { + /// Name of the array to define. Type annotations are not allowed, hence why this is not a + /// `VarRef`. + pub name: String, + + /// Position of the name. + pub name_pos: LineCol, + + /// Whether the array is global or not. + pub shared: bool, + + /// Expressions to compute the dimensions of the array. + pub dimensions: Vec, + + /// Type of the array to be defined. + pub subtype: ExprType, + + /// Position of the subtype. + pub subtype_pos: LineCol, +} + +/// Type of the `DO` loop. +#[derive(Debug, PartialEq)] +pub enum DoGuard { + /// Represents an infinite loop without guards. + Infinite, + + /// Represents a loop with an `UNTIL` guard in the `DO` clause. + PreUntil(Expr), + + /// Represents a loop with a `WHILE` guard in the `DO` clause. + PreWhile(Expr), + + /// Represents a loop with an `UNTIL` guard in the `LOOP` clause. + PostUntil(Expr), + + /// Represents a loop with a `WHILE` guard in the `LOOP` clause. + PostWhile(Expr), +} + +/// Components of a `DO` statement. +#[derive(Debug, PartialEq)] +pub struct DoSpan { + /// Expression to compute whether to execute the loop's body or not and where this appears in + /// the `DO` statement. + pub guard: DoGuard, + + /// Statements within the loop's body. + pub body: Vec, +} + +/// Components of an `END` statement. +#[derive(Debug, PartialEq)] +pub struct EndSpan { + /// Integer expression to compute the return code. + pub code: Option, +} + +/// Components of an `EXIT` statement. +#[derive(Debug, Eq, PartialEq)] +pub struct ExitSpan { + /// Position of the statement. + pub pos: LineCol, +} + +/// Components of a branch of an `IF` statement. +#[derive(Debug, PartialEq)] +pub struct IfBranchSpan { + /// Expression that guards execution of this branch. + pub guard: Expr, + + /// Statements within the branch. + pub body: Vec, +} + +/// Components of an `IF` statement. +#[derive(Debug, PartialEq)] +pub struct IfSpan { + /// Sequence of the branches in the conditional. + /// + /// Representation of the conditional branches. The final `ELSE` branch, if present, is also + /// included here and its guard clause is always a true expression. + pub branches: Vec, +} + +/// Components of a `FOR` statement. +/// +/// Note that we do not store the original end and step values, and instead use expressions to +/// represent the loop condition and the computation of the next iterator value. We do this +/// for run-time efficiency. The reason this is possible is because we force the step to be an +/// integer literal at parse time and do not allow it to be an expression. +#[derive(Debug, PartialEq)] +pub struct ForSpan { + /// Iterator name, expressed as a variable reference that must be either automatic or an + /// integer. + pub iter: VarRef, + + /// Position of the iterator. + pub iter_pos: LineCol, + + /// If true, the iterator computation needs to be performed as a double so that, when the + /// iterator variable is not yet defined, it gains the correct type. + pub iter_double: bool, + + /// Expression to compute the iterator's initial value. + pub start: Expr, + + /// Condition to test after each iteration. + pub end: Expr, + + /// Expression to compute the iterator's next value. + pub next: Expr, + + /// Statements within the loop's body. + pub body: Vec, +} + +/// Components of a `GOTO` or a `GOSUB` statement. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GotoSpan { + /// Name of the label to jump to. + pub target: String, + + /// Position of the label. + pub target_pos: LineCol, +} + +/// Components of a label "statement". +/// +/// In principle, labels should be just a property of a statement but, for simplicity in the +/// current model, it's easiest to represent them as their own statement. +#[derive(Debug, Eq, PartialEq)] +pub struct LabelSpan { + /// Name of the label being defined. + pub name: String, + + /// Position of the label. + pub name_pos: LineCol, +} + +/// Components of an `ON ERROR` statement. +#[derive(Debug, Eq, PartialEq)] +pub enum OnErrorSpan { + /// Components of an `ON ERROR GOTO @label` statement. + Goto(GotoSpan), + + /// Components of an `ON ERROR GOTO 0` statement. + Reset, + + /// Components of an `ON ERROR RESUME NEXT` statement. + ResumeNext, +} + +/// Components of a `RETURN` statement. +#[derive(Debug, Eq, PartialEq)] +pub struct ReturnSpan { + /// Position of the statement. + pub pos: LineCol, +} + +/// Collection of relational operators that can appear in a `CASE IS` guard.. +#[derive(Debug, Eq, PartialEq)] +pub enum CaseRelOp { + /// Relational operator for `CASE IS =`. + Equal, + + /// Relational operator for `CASE IS <>`. + NotEqual, + + /// Relational operator for `CASE IS <`. + Less, + + /// Relational operator for `CASE IS <=`. + LessEqual, + + /// Relational operator for `CASE IS >`. + Greater, + + /// Relational operator for `CASE IS >=`. + GreaterEqual, +} + +/// Components of a `CASE` guard. +#[derive(Debug, PartialEq)] +pub enum CaseGuardSpan { + /// Represents an `IS ` guard or a simpler `` guard. + Is(CaseRelOp, Expr), + + /// Represents an ` TO ` guard. + To(Expr, Expr), +} + +/// Components of a branch of a `SELECT` statement. +#[derive(Debug, PartialEq)] +pub struct CaseSpan { + /// Expressions that guard execution of this case. + pub guards: Vec, + + /// Statements within the case block. + pub body: Vec, +} + +/// Components of a `SELECT` statement. +#[derive(Debug, PartialEq)] +pub struct SelectSpan { + /// Expression to test for. + pub expr: Expr, + + /// Representation of the cases to select from. The final `CASE ELSE`, if present, is also + /// included here without any guards. + pub cases: Vec, + + /// Position of the `END SELECT` statement. + pub end_pos: LineCol, +} + +/// Components of a `WHILE` statement. +#[derive(Debug, PartialEq)] +pub struct WhileSpan { + /// Expression to compute whether to execute the loop's body or not. + pub expr: Expr, + + /// Statements within the loop's body. + pub body: Vec, +} + +/// Represents a statement in the program along all data to execute it. +#[derive(Debug, PartialEq)] +pub enum Statement { + /// Represents an assignment to an element of an array. + ArrayAssignment(ArrayAssignmentSpan), + + /// Represents a variable assignment. + Assignment(AssignmentSpan), + + /// Represents a call to a builtin command such as `PRINT`. + Call(CallSpan), + + /// Represents a `FUNCTION` or `SUB` definition. The difference between the two lies in just + /// the presence or absence of a return type in the callable. + Callable(CallableSpan), + + /// Represents a `DATA` statement. + Data(DataSpan), + + /// Represents a variable definition. + Dim(DimSpan), + + /// Represents an array definition. + DimArray(DimArraySpan), + + /// Represents a `DO` statement. + Do(DoSpan), + + /// Represents an `END` statement. + End(EndSpan), + + /// Represents an `EXIT DO` statement. + ExitDo(ExitSpan), + + /// Represents an `EXIT FOR` statement. + ExitFor(ExitSpan), + + /// Represents an `EXIT FUNCTION` statement. + ExitFunction(ExitSpan), + + /// Represents an `EXIT SUB` statement. + ExitSub(ExitSpan), + + /// Represents a `FOR` statement. + For(ForSpan), + + /// Represents a `GOSUB` statement. + Gosub(GotoSpan), + + /// Represents a `GOTO` statement. + Goto(GotoSpan), + + /// Represents an `IF` statement. + If(IfSpan), + + /// Represents a label "statement". + Label(LabelSpan), + + /// Represents an `ON ERROR` statement. + OnError(OnErrorSpan), + + /// Represents a `RETURN` statement. + Return(ReturnSpan), + + /// Represents a `SELECT` statement. + Select(SelectSpan), + + /// Represents a `WHILE` statement. + While(WhileSpan), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_varref_display() { + assert_eq!("name", format!("{}", VarRef::new("name", None))); + assert_eq!("abc?", format!("{}", VarRef::new("abc", Some(ExprType::Boolean)))); + assert_eq!("cba#", format!("{}", VarRef::new("cba", Some(ExprType::Double)))); + assert_eq!("def%", format!("{}", VarRef::new("def", Some(ExprType::Integer)))); + assert_eq!("ghi$", format!("{}", VarRef::new("ghi", Some(ExprType::Text)))); + } + + #[test] + fn test_varref_accepts() { + assert!(VarRef::new("a", None).accepts(ExprType::Boolean)); + assert!(VarRef::new("a", None).accepts(ExprType::Double)); + assert!(VarRef::new("a", None).accepts(ExprType::Integer)); + assert!(VarRef::new("a", None).accepts(ExprType::Text)); + + assert!(VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Boolean)); + assert!(!VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Double)); + assert!(!VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Integer)); + assert!(!VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Text)); + + assert!(!VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Boolean)); + assert!(VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Double)); + assert!(!VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Integer)); + assert!(!VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Text)); + + assert!(!VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Boolean)); + assert!(!VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Double)); + assert!(VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Integer)); + assert!(!VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Text)); + + assert!(!VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Boolean)); + assert!(!VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Double)); + assert!(!VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Integer)); + assert!(VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Text)); + } +} diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs new file mode 100644 index 00000000..ab406b61 --- /dev/null +++ b/core2/src/bytecode.rs @@ -0,0 +1,304 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Bytecode for a compiled EndBASIC program. + +use crate::convert::cast; +use std::fmt; + +pub(crate) const MAX_GLOBAL_REGISTERS: u8 = 128; + +trait RawValue { + fn from_u32(v: u32) -> Self; + fn to_u32(self) -> u32; +} + +macro_rules! raw_value_impl { + ( $ty:ty ) => { + impl RawValue for $ty { + fn from_u32(v: u32) -> Self { + cast(v) + } + + fn to_u32(self) -> u32 { + cast(self) + } + } + }; +} + +raw_value_impl!(u8); +raw_value_impl!(i16); +raw_value_impl!(u16); +raw_value_impl!(usize); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct IRegister(pub(crate) u8); + +impl fmt::Display for IRegister { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "R{}", self.0) + } +} + +impl RawValue for IRegister { + fn from_u32(v: u32) -> Self { + Self(v as u8) + } + + fn to_u32(self) -> u32 { + u32::from(self.0) + } +} + +impl IRegister { + pub(crate) fn to_index(self, fp: usize) -> usize { + let reg: usize = cast(self.0); + let max: usize = cast(MAX_GLOBAL_REGISTERS); + if reg < max { cast(reg) } else { reg - max + fp } + } +} + +pub(crate) type Register = u8; + +#[repr(u8)] +pub(crate) enum Opcode { + Nop, + + Enter, + Leave, + + Upcall, + + Move, + + LoadInteger, + LoadIntegerConstant, + + AddInteger, +} + +macro_rules! instr { + ( $opcode:expr, $name:expr, + $make:ident, $parse:ident, $format:ident, + ) => { + pub(crate) fn $make() -> u32 { + ($opcode as u32) << 24 + } + + pub(crate) fn $parse(op: u32) { + debug_assert_eq!($opcode as u32, op >> 24); + } + + pub(crate) fn $format(op: u32) -> String { + $parse(op); + $name.to_owned() + } + }; + + ( $opcode:expr, $name: expr, + $make:ident, $parse:ident, $format:ident, + $type1:ty, $mask1:expr, $offset1:expr, + ) => { + pub(crate) fn $make(v1: $type1) -> u32 { + let v1 = (RawValue::to_u32(v1) & $mask1) << $offset1; + (($opcode as u32) << 24) | v1 + } + + pub(crate) fn $parse(op: u32) -> $type1 { + debug_assert_eq!($opcode as u32, op >> 24); + let v1 = RawValue::from_u32((op >> $offset1) & $mask1); + v1 + } + + pub(crate) fn $format(op: u32) -> String { + let v1 = $parse(op); + format!("{} {}", $name, v1) + } + }; + + ( $opcode:expr, $name:expr, + $make:ident, $parse:ident, $format:ident, + $type1:ty, $mask1:expr, $offset1:expr, + $type2:ty, $mask2:expr, $offset2:expr, + ) => { + pub(crate) fn $make(v1: $type1, v2: $type2) -> u32 { + let v1 = (RawValue::to_u32(v1) & $mask1) << $offset1; + let v2 = (RawValue::to_u32(v2) & $mask2) << $offset2; + (($opcode as u32) << 24) | v1 | v2 + } + + pub(crate) fn $parse(op: u32) -> ($type1, $type2) { + debug_assert_eq!($opcode as u32, op >> 24); + let v1 = RawValue::from_u32((op >> $offset1) & $mask1); + let v2 = RawValue::from_u32((op >> $offset2) & $mask2); + (v1, v2) + } + + pub(crate) fn $format(op: u32) -> String { + let (v1, v2) = $parse(op); + format!("{} {}, {}", $name, v1, v2) + } + }; + + ( $opcode:expr, $name:expr, + $make:ident, $parse:ident, $format:ident, + $type1:ty, $mask1:expr, $offset1:expr, + $type2:ty, $mask2:expr, $offset2:expr, + $type3:ty, $mask3:expr, $offset3:expr, + ) => { + pub(crate) fn $make(v1: $type1, v2: $type2, v3: $type3) -> u32 { + let v1 = (RawValue::to_u32(v1) & $mask1) << $offset1; + let v2 = (RawValue::to_u32(v2) & $mask2) << $offset2; + let v3 = (RawValue::to_u32(v3) & $mask3) << $offset3; + (($opcode as u32) << 24) | v1 | v2 | v3 + } + + pub(crate) fn $parse(op: u32) -> ($type1, $type2, $type3) { + debug_assert_eq!($opcode as u32, op >> 24); + let v1 = RawValue::from_u32((op >> $offset1) & $mask1); + let v2 = RawValue::from_u32((op >> $offset2) & $mask2); + let v3 = RawValue::from_u32((op >> $offset3) & $mask3); + (v1, v2, v3) + } + + pub(crate) fn $format(op: u32) -> String { + let (v1, v2, v3) = $parse(op); + format!("{} {}, {}, {}", $name, v1, v2, v3) + } + }; +} + +#[rustfmt::skip] +instr!( + Opcode::Nop, "NOP", + make_nop, parse_nop, format_nop, +); + +#[rustfmt::skip] +instr!( + Opcode::Enter, "ENTER", + make_enter, parse_enter, format_enter, + usize, 0x0000ffff, 0, // Number of local registers to allocate. +); +#[rustfmt::skip] +instr!( + Opcode::Leave, "LEAVE", + make_leave, parse_leave, format_leave, +); + +#[rustfmt::skip] +instr!( + Opcode::Upcall, "UPCALL", + make_upcall, parse_upcall, format_upcall, + usize, 0x0000ffff, 8, // Index of the upcall to execute. + usize, 0x000000ff, 0, // Number of arguments. +); + +#[rustfmt::skip] +instr!( + Opcode::Move, "MOVE", + make_move, parse_move, format_move, + IRegister, 0x000000ff, 8, + IRegister, 0x000000ff, 0, +); + +#[rustfmt::skip] +instr!( + Opcode::LoadInteger, "LOADIIMM", + make_load_integer, parse_load_integer, format_load_integer, + IRegister, 0x000000ff, 16, + i16, 0x0000ffff, 0, +); +#[rustfmt::skip] +instr!( + Opcode::LoadIntegerConstant, "LOADICONST", + make_load_integer_constant, parse_load_integer_constant, format_load_integer_constant, + IRegister, 0x000000ff, 16, + u16, 0x0000ffff, 0, +); + +#[rustfmt::skip] +instr!( + Opcode::AddInteger, "ADDI", + make_add_integer, parse_add_integer, format_add_integer, + IRegister, 0x000000ff, 16, + IRegister, 0x000000ff, 8, + IRegister, 0x000000ff, 0, +); + +pub(crate) const INSTR_FORMATTERS: &[fn(u32) -> String] = &[ + format_nop, + format_enter, + format_leave, + format_upcall, + format_move, + format_load_integer, + format_load_integer_constant, + format_add_integer, +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nop() { + let instr = make_nop(); + parse_nop(instr); + } + + #[test] + fn test_enter() { + let instr = make_enter(10); + assert_eq!(10, parse_enter(instr)); + } + + #[test] + fn test_leave() { + let instr = make_leave(); + parse_leave(instr); + } + + #[test] + fn test_upcall() { + let instr = make_upcall(512); + assert_eq!(512, parse_upcall(instr)); + } + + #[test] + fn test_move() { + let instr = make_move(10, 20); + assert_eq!((10, 20), parse_move(instr)); + } + + #[test] + fn test_load_integer() { + let instr = make_load_integer(1, 12345); + assert_eq!((1, 12345), parse_load_integer(instr)); + } + + #[test] + fn test_load_integer_constant() { + let instr = make_load_integer_constant(1, 2); + assert_eq!((1, 2), parse_load_integer_constant(instr)); + } + + #[test] + fn test_add_integer() { + let instr = make_add_integer(1, 2, 3); + assert_eq!((1, 2, 3), parse_add_integer(instr)); + } +} diff --git a/core2/src/callable.rs b/core2/src/callable.rs new file mode 100644 index 00000000..7ff2aef3 --- /dev/null +++ b/core2/src/callable.rs @@ -0,0 +1,551 @@ +// EndBASIC +// Copyright 2021 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Symbol definitions and symbols table representation. + +use crate::ast::ArgSep; +use crate::ast::ExprType; +use crate::bytecode::Register; +use async_trait::async_trait; +use std::borrow::Cow; +use std::ops::RangeInclusive; +use std::str::Lines; + +/// Details to compile a required scalar parameter. +#[derive(Clone, Debug)] +pub struct RequiredValueSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// The type of the expected parameter. + pub vtype: ExprType, +} + +/// Details to compile a required reference parameter. +#[derive(Clone, Debug)] +pub struct RequiredRefSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// If true, require an array reference; if false, a variable reference. + pub require_array: bool, + + /// If true, allow references to undefined variables because the command will define them when + /// missing. Can only be set to true for commands, not functions, and `require_array` must be + /// false. + pub define_undefined: bool, +} + +/// Details to compile an optional scalar parameter. +/// +/// Optional parameters are only supported in commands. +#[derive(Clone, Debug)] +pub struct OptionalValueSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// The type of the expected parameter. + pub vtype: ExprType, + + /// Value to push onto the stack when the parameter is missing. + pub missing_value: i32, + + /// Value to push onto the stack when the parameter is present, after which the stack contains + /// the parameter value. + pub present_value: i32, +} + +/// Details to describe the type of a repeated parameter. +#[derive(Clone, Debug)] +pub enum RepeatedTypeSyntax { + /// Allows any value type, including empty arguments. The values pushed onto the stack have + /// the same semantics as those pushed by `AnyValueSyntax`. + AnyValue, + + /// Expects a value of the given type. + TypedValue(ExprType), + + /// Expects a reference to a variable (not an array) and allows the variables to not be defined. + VariableRef, +} + +/// Details to compile a repeated parameter. +/// +/// The repeated parameter must appear after all singular positional parameters. +#[derive(Clone, Debug)] +pub struct RepeatedSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// The type of the expected parameters. + pub type_syn: RepeatedTypeSyntax, + + /// The separator to expect between the repeated parameters. For functions, this must be the + /// long separator (the comma). + pub sep: ArgSepSyntax, + + /// Whether the repeated parameter must at least have one element or not. + pub require_one: bool, + + /// Whether to allow any parameter to not be present or not. Can only be true for commands. + pub allow_missing: bool, +} + +impl RepeatedSyntax { + /// Formats the repeated argument syntax for help purposes into `output`. + /// + /// `last_singular_sep` contains the separator of the last singular argument syntax, if any, + /// which we need to place inside of the optional group. + fn describe(&self, output: &mut String, last_singular_sep: Option<&ArgSepSyntax>) { + if !self.require_one { + output.push('['); + } + + if let Some(sep) = last_singular_sep { + sep.describe(output); + } + + output.push_str(&self.name); + output.push('1'); + if let RepeatedTypeSyntax::TypedValue(vtype) = self.type_syn { + output.push(vtype.annotation()); + } + + if self.require_one { + output.push('['); + } + + self.sep.describe(output); + output.push_str(".."); + self.sep.describe(output); + + output.push_str(&self.name); + output.push('N'); + if let RepeatedTypeSyntax::TypedValue(vtype) = self.type_syn { + output.push(vtype.annotation()); + } + + output.push(']'); + } +} + +/// Details to compile a parameter of any scalar type. +#[derive(Clone, Debug)] +pub struct AnyValueSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// Whether to allow the parameter to not be present or not. Can only be true for commands. + pub allow_missing: bool, +} + +/// Details to process an argument separator. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ArgSepSyntax { + /// The argument separator must exactly be the one give. + Exactly(ArgSep), + + /// The argument separator may be any of the ones given. + OneOf(ArgSep, ArgSep), + + /// The argument separator is the end of the call. + End, +} + +impl ArgSepSyntax { + /// Formats the argument separator for help purposes into `output`. + fn describe(&self, output: &mut String) { + match self { + ArgSepSyntax::Exactly(sep) => { + let (text, needs_space) = sep.describe(); + + if !text.is_empty() && needs_space { + output.push(' '); + } + output.push_str(text); + if !text.is_empty() { + output.push(' '); + } + } + + ArgSepSyntax::OneOf(sep1, sep2) => { + let (text1, _needs_space1) = sep1.describe(); + let (text2, _needs_space2) = sep2.describe(); + + output.push(' '); + output.push_str(&format!("<{}|{}>", text1, text2)); + output.push(' '); + } + + ArgSepSyntax::End => (), + }; + } +} + +/// Details to process a non-repeated argument. +/// +/// Every item in this enum is composed of a struct that provides the details on the parameter and +/// a struct that provides the details on how this parameter is separated from the next. +#[derive(Clone, Debug)] +pub enum SingularArgSyntax { + /// A required scalar value. + RequiredValue(RequiredValueSyntax, ArgSepSyntax), + + /// A required reference. + RequiredRef(RequiredRefSyntax, ArgSepSyntax), + + /// An optional scalar value. + OptionalValue(OptionalValueSyntax, ArgSepSyntax), + + /// A required scalar value of any type. + AnyValue(AnyValueSyntax, ArgSepSyntax), +} + +/// Details to process the arguments of a callable. +/// +/// Note that the description of function arguments is more restricted than that of commands. +/// The arguments compiler panics when these preconditions aren't met with the rationale that +/// builtin functions must never be ill-defined. +// TODO(jmmv): It might be nice to try to express these restrictions in the type system, but +// things are already too verbose as they are... +#[derive(Clone, Debug)] +pub(crate) struct CallableSyntax { + /// Ordered list of singular arguments that appear before repeated arguments. + singular: Cow<'static, [SingularArgSyntax]>, + + /// Details on the repeated argument allowed after singular arguments. + repeated: Option>, +} + +impl CallableSyntax { + /// Creates a new callable arguments definition from its parts defined statically in the + /// code. + pub(crate) fn new_static( + singular: &'static [SingularArgSyntax], + repeated: Option<&'static RepeatedSyntax>, + ) -> Self { + Self { singular: Cow::Borrowed(singular), repeated: repeated.map(Cow::Borrowed) } + } + + /// Creates a new callable arguments definition from its parts defined dynamically at + /// runtime. + pub(crate) fn new_dynamic( + singular: Vec, + repeated: Option, + ) -> Self { + Self { singular: Cow::Owned(singular), repeated: repeated.map(Cow::Owned) } + } + + /// Computes the range of the expected number of parameters for this syntax. + fn expected_nargs(&self) -> RangeInclusive { + let mut min = self.singular.len(); + let mut max = self.singular.len(); + if let Some(syn) = self.repeated.as_ref() { + if syn.require_one { + min += 1; + } + max = usize::MAX; + } + min..=max + } + + /// Returns true if this syntax represents "no arguments". + pub(crate) fn is_empty(&self) -> bool { + self.singular.is_empty() && self.repeated.is_none() + } + + /// Produces a user-friendly description of this callable syntax. + pub(crate) fn describe(&self) -> String { + let mut description = String::new(); + let mut last_singular_sep = None; + for (i, s) in self.singular.iter().enumerate() { + let sep = match s { + SingularArgSyntax::RequiredValue(details, sep) => { + description.push_str(&details.name); + description.push(details.vtype.annotation()); + sep + } + + SingularArgSyntax::RequiredRef(details, sep) => { + description.push_str(&details.name); + sep + } + + SingularArgSyntax::OptionalValue(details, sep) => { + description.push('['); + description.push_str(&details.name); + description.push(details.vtype.annotation()); + description.push(']'); + sep + } + + SingularArgSyntax::AnyValue(details, sep) => { + if details.allow_missing { + description.push('['); + } + description.push_str(&details.name); + if details.allow_missing { + description.push(']'); + } + sep + } + }; + + if self.repeated.is_none() || i < self.singular.len() - 1 { + sep.describe(&mut description); + } + if i == self.singular.len() - 1 { + last_singular_sep = Some(sep); + } + } + + if let Some(syn) = &self.repeated { + syn.describe(&mut description, last_singular_sep); + } + + description + } +} + +/// Builder pattern for a callable's metadata. +pub struct CallableMetadataBuilder { + name: Cow<'static, str>, + return_type: Option, + category: Option<&'static str>, + syntaxes: Vec, + description: Option<&'static str>, +} + +impl CallableMetadataBuilder { + /// Constructs a new metadata builder with the minimum information necessary. + /// + /// All code except tests must populate the whole builder with details. This is enforced at + /// construction time, where we only allow some fields to be missing under the test + /// configuration. + pub fn new(name: &'static str) -> Self { + assert!(name == name.to_ascii_uppercase(), "Callable name must be in uppercase"); + + Self { + name: Cow::Borrowed(name), + return_type: None, + syntaxes: vec![], + category: None, + description: None, + } + } + + /// Constructs a new metadata builder with the minimum information necessary. + /// + /// This is the same as `new` but using a dynamically-allocated name, which is necessary for + /// user-defined symbols. + pub fn new_dynamic>(name: S) -> Self { + Self { + name: Cow::Owned(name.into().to_ascii_uppercase()), + return_type: None, + syntaxes: vec![], + category: Some("User defined"), + description: Some("User defined symbol."), + } + } + + /// Sets the return type of the callable. + pub fn with_return_type(mut self, return_type: ExprType) -> Self { + self.return_type = Some(return_type); + self + } + + /// Sets the syntax specifications for this callable. + pub fn with_syntax( + mut self, + syntaxes: &'static [(&'static [SingularArgSyntax], Option<&'static RepeatedSyntax>)], + ) -> Self { + self.syntaxes = syntaxes + .iter() + .map(|s| CallableSyntax::new_static(s.0, s.1)) + .collect::>(); + self + } + + /// Sets the syntax specifications for this callable. + pub(crate) fn with_syntaxes>>(mut self, syntaxes: S) -> Self { + self.syntaxes = syntaxes.into(); + self + } + + /// Sets the syntax specifications for this callable. + pub(crate) fn with_dynamic_syntax( + self, + syntaxes: Vec<(Vec, Option)>, + ) -> Self { + let syntaxes = syntaxes + .into_iter() + .map(|s| CallableSyntax::new_dynamic(s.0, s.1)) + .collect::>(); + self.with_syntaxes(syntaxes) + } + + /// Sets the category for this callable. All callables with the same category name will be + /// grouped together in help messages. + pub fn with_category(mut self, category: &'static str) -> Self { + self.category = Some(category); + self + } + + /// Sets the description for this callable. The `description` is a collection of paragraphs + /// separated by a single newline character, where the first paragraph is taken as the summary + /// of the description. The summary must be a short sentence that is descriptive enough to be + /// understood without further details. Empty lines (paragraphs) are not allowed. + pub fn with_description(mut self, description: &'static str) -> Self { + for l in description.lines() { + assert!(!l.is_empty(), "Description cannot contain empty lines"); + } + self.description = Some(description); + self + } + + /// Generates the final `CallableMetadata` object, ensuring all values are present. + pub fn build(self) -> CallableMetadata { + assert!(!self.syntaxes.is_empty(), "All callables must specify a syntax"); + CallableMetadata { + name: self.name, + return_type: self.return_type, + syntaxes: self.syntaxes, + category: self.category.expect("All callables must specify a category"), + description: self.description.expect("All callables must specify a description"), + } + } + + /// Generates the final `CallableMetadata` object, ensuring the minimal set of values are + /// present. Only useful for testing. + pub fn test_build(mut self) -> CallableMetadata { + if self.syntaxes.is_empty() { + self.syntaxes.push(CallableSyntax::new_static(&[], None)); + } + CallableMetadata { + name: self.name, + return_type: self.return_type, + syntaxes: self.syntaxes, + category: self.category.unwrap_or(""), + description: self.description.unwrap_or(""), + } + } +} + +/// Representation of a callable's metadata. +/// +/// The callable is expected to hold onto an instance of this object within its struct to make +/// queries fast. +#[derive(Clone, Debug)] +pub struct CallableMetadata { + name: Cow<'static, str>, + return_type: Option, + syntaxes: Vec, + category: &'static str, + description: &'static str, +} + +impl CallableMetadata { + /// Gets the callable's name, all in uppercase. + pub fn name(&self) -> &str { + &self.name + } + + /// Gets the callable's return type. + pub fn return_type(&self) -> Option { + self.return_type + } + + /// Gets the callable's syntax specification. + pub fn syntax(&self) -> String { + fn format_one(cs: &CallableSyntax) -> String { + let mut syntax = cs.describe(); + if syntax.is_empty() { + syntax.push_str("no arguments"); + } + syntax + } + + match self.syntaxes.as_slice() { + [] => panic!("Callables without syntaxes are not allowed at construction time"), + [one] => format_one(one), + many => many + .iter() + .map(|syn| format!("<{}>", syn.describe())) + .collect::>() + .join(" | "), + } + } + + /// Returns the callable's syntax definitions. + pub(crate) fn syntaxes(&self) -> &[CallableSyntax] { + &self.syntaxes + } + + /// Gets the callable's category as a collection of lines. The first line is the title of the + /// category, and any extra lines are additional information for it. + pub fn category(&self) -> &'static str { + self.category + } + + /// Gets the callable's textual description as a collection of lines. The first line is the + /// summary of the callable's purpose. + pub fn description(&self) -> Lines<'static> { + self.description.lines() + } + + /// Returns true if this is a callable that takes no arguments. + pub fn is_argless(&self) -> bool { + self.syntaxes.is_empty() || (self.syntaxes.len() == 1 && self.syntaxes[0].is_empty()) + } + + /// Returns true if this callable is a function (not a command). + pub fn is_function(&self) -> bool { + self.return_type.is_some() + } +} + +pub struct Scope<'a> { + pub(crate) regs: &'a [u32], +} + +impl<'a> Scope<'a> { + pub fn get_integer(&self, arg: Register) -> i32 { + self.regs[arg as usize] as i32 + } +} + +/// A trait to define a callable that is executed by a `Machine`. +/// +/// The callable themselves are immutable but they can reference mutable state. Given that +/// EndBASIC is not threaded, it is sufficient for those references to be behind a `RefCell` +/// and/or an `Rc`. +/// +/// Idiomatically, these objects need to provide a `new()` method that returns an `Rc`, as +/// that's the type used throughout the execution engine. +#[async_trait(?Send)] +pub trait Callable { + /// Returns the metadata for this function. + /// + /// The return value takes the form of a reference to force the callable to store the metadata + /// as a struct field so that calls to this function are guaranteed to be cheap. + fn metadata(&self) -> &CallableMetadata; + + /// Executes the function. + /// + /// `args` contains the arguments to the function call. + /// + /// `machine` provides mutable access to the current state of the machine invoking the function. + async fn exec(&self, scope: Scope<'_>); +} diff --git a/core2/src/compiler/ids.rs b/core2/src/compiler/ids.rs new file mode 100644 index 00000000..60ec6d35 --- /dev/null +++ b/core2/src/compiler/ids.rs @@ -0,0 +1,125 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! ID generators. + +use std::collections::HashMap; +use std::hash::Hash; + +/// Trait to convert an usize to a narrower integer type. +pub(super) trait IdConverter: Sized { + fn convert(v: usize) -> Self; +} + +impl IdConverter for usize { + fn convert(v: usize) -> Self { + v + } +} + +impl IdConverter for u8 { + fn convert(v: usize) -> Self { + debug_assert!(v <= (u8::MAX as usize)); + v as u8 + } +} + +/// Data structure with O(1) lookup and insertion that assigns sequential identifiers to +/// elements as they are inserted and allows later retrieval of these identifiers and +/// allows retrieving the inserted values in insertion order. +/// +/// Identifiers are assigned starting at `base`. +pub(super) struct IdAssigner { + map: HashMap, + base: usize, +} + +impl IdAssigner { + pub(crate) fn new(base: usize) -> Self { + Self { map: HashMap::default(), base } + } +} + +impl IdAssigner { + /// Gets the identifier for a `key`, assigning one if the `key` does not yet have one. + // DO NOT SUBMIT: This should take something else to support both &T and T as input... + // I tried Into but couldn't get it to work. + pub(super) fn get(&mut self, key: &T) -> I { + if let Some(index) = self.map.get(key) { + *index + } else { + let index = self.base + self.map.len(); + let index = I::convert(index); + self.map.insert(key.clone(), index); + index + } + } + + /// Returns the number of assigned identifiers. + pub(super) fn len(&self) -> usize { + self.map.len() + } + + /// Returns the keys in insertion order. + pub(super) fn to_vec(self) -> Vec { + let mut reverse = self.map.into_iter().collect::>(); + reverse.sort_by_key(|(_value, index)| *index); + reverse.into_iter().map(|(value, _index)| value).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_id_assigner_with_u8_ids() { + let mut map = IdAssigner::<&'static str, u8>::new(0); + + assert_eq!(0, map.get("foo")); + assert_eq!(1, map.get("bar")); + assert_eq!(2, map.get("baz")); + + assert_eq!(1, map.get("bar")); + + assert_eq!(["foo", "bar", "baz"], map.to_vec().as_slice()); + } + + #[test] + fn test_id_assigner_with_usize_ids() { + let mut map = IdAssigner::<&'static str, usize>::new(0); + + assert_eq!(0, map.get("foo")); + assert_eq!(1, map.get("bar")); + assert_eq!(2, map.get("baz")); + + assert_eq!(1, map.get("bar")); + + assert_eq!(["foo", "bar", "baz"], map.to_vec().as_slice()); + } + + #[test] + fn test_id_assigner_with_base() { + let mut map = IdAssigner::<&'static str, u8>::new(128); + + assert_eq!(128, map.get("foo")); + assert_eq!(129, map.get("bar")); + assert_eq!(130, map.get("baz")); + + assert_eq!(129, map.get("bar")); + + assert_eq!(["foo", "bar", "baz"], map.to_vec().as_slice()); + } +} diff --git a/core2/src/compiler/image.rs b/core2/src/compiler/image.rs new file mode 100644 index 00000000..edccdeee --- /dev/null +++ b/core2/src/compiler/image.rs @@ -0,0 +1,48 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Compiled program representation. + +use crate::callable::Callable; +use crate::compiler::SymbolKey; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Clone, Eq, Hash, PartialEq)] +pub(crate) enum Constant { + //Double(f64), + Integer(i32), + Text(String), +} + +pub struct Image { + pub(crate) code: Vec, + pub(crate) data: Vec, + pub(crate) upcalls: Vec, + pub(crate) constants: Vec, +} + +impl Image { + pub fn map_upcalls( + &self, + upcalls_by_name: &HashMap>, + ) -> Vec> { + let mut upcalls = Vec::with_capacity(self.upcalls.len()); + for key in &self.upcalls { + upcalls.push(upcalls_by_name.get(&key).unwrap().clone()); + } + upcalls + } +} diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs new file mode 100644 index 00000000..275f31f4 --- /dev/null +++ b/core2/src/compiler/mod.rs @@ -0,0 +1,169 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Compiler for the EndBASIC language into bytecode. + +use crate::ast::{ArgSpan, Expr, Statement}; +use crate::bytecode::{self, IRegister}; +use crate::callable::Callable; +use crate::parser; +use crate::reader::LineCol; +use std::collections::HashMap; +use std::io; +use std::rc::Rc; + +mod image; +pub(crate) use image::{Constant, Image}; + +mod ids; +use ids::IdAssigner; + +mod syms; +pub use syms::SymbolKey; +use syms::Symtable; + +/// Compilation errors. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] // The error messages and names are good enough. +pub enum Error { + #[error("{0}: I/O error during compilation: {1}")] + IoError(LineCol, io::Error), + + #[error("{0}: {1}")] + ParseError(LineCol, String), +} + +type Result = std::result::Result; + +impl From for Error { + fn from(value: parser::Error) -> Self { + match value { + parser::Error::Bad(pos, message) => Self::ParseError(pos, message), + parser::Error::Io(pos, e) => Self::IoError(pos, e), + } + } +} + +type Address = usize; + +enum Fixup { + Enter(usize), +} + +struct Context<'a> { + upcalls_by_name: &'a HashMap>, + upcalls: IdAssigner, + code: Vec, + constants: IdAssigner, + symtable: Symtable, + fixups: HashMap, +} + +impl<'a> Context<'a> { + fn constant(&mut self, constant: Constant) -> usize { + self.constants.get(&constant) + } + + fn upcall(&mut self, key: SymbolKey) -> usize { + self.upcalls.get(&key) + } + + fn emit(&mut self, op: u32) -> Address { + self.code.push(op); + self.code.len() - 1 + } + + fn to_image(self) -> Image { + Image { + code: self.code, + data: vec![], + upcalls: self.upcalls.to_vec(), + constants: self.constants.to_vec(), + } + } +} + +fn compile_expr(context: &mut Context, reg: IRegister, expr: Expr) { + match expr { + Expr::Integer(span) => { + if span.value < (0x00ffffff as i32) { + context.emit(bytecode::make_load_integer(reg, span.value as i16)); + } else { + let index = context.constant(Constant::Integer(span.value)); + context.emit(bytecode::make_load_integer_constant(reg, index as u16)); + } + } + + Expr::Add(span) => { + compile_expr(context, IRegister(reg.0 + 1), span.lhs); + compile_expr(context, IRegister(reg.0 + 2), span.rhs); + context.emit(bytecode::make_add_integer( + reg, + IRegister(reg.0 + 1), + IRegister(reg.0 + 2), + )); + } + + _ => todo!(), + } +} + +pub fn compile( + input: &mut dyn io::Read, + upcalls_by_name: &HashMap>, +) -> Result { + let mut context = Context { + upcalls_by_name, + upcalls: IdAssigner::new(0), + code: vec![], + constants: IdAssigner::new(0), + symtable: Symtable::default(), + fixups: HashMap::default(), + }; + + let enter = context.emit(bytecode::make_nop()); + for stmt in parser::parse(input) { + match stmt? { + Statement::Assignment(span) => { + let key = SymbolKey::from(span.vref.name); + let local = context.symtable.local(&key); + compile_expr(&mut context, IRegister(local), span.expr); + } + + Statement::Call(span) => { + let key = SymbolKey::from(span.vref.name); + let nargs = span.args.len(); + for ArgSpan { expr, sep, sep_pos } in span.args { + compile_expr(&mut context, IRegister(128), expr.unwrap()); + } + let upcall = context.upcall(key); + context.emit(bytecode::make_upcall(upcall, nargs)); + } + + _ => todo!(), + } + } + context.emit(bytecode::make_leave()); + context.fixups.insert(enter, Fixup::Enter(context.symtable.nlocals())); + + for (addr, fixup) in &context.fixups { + let instr = match fixup { + Fixup::Enter(nargs) => bytecode::make_enter(*nargs), + }; + context.code[*addr] = instr; + } + + Ok(context.to_image()) +} diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs new file mode 100644 index 00000000..4ebe62c4 --- /dev/null +++ b/core2/src/compiler/syms.rs @@ -0,0 +1,65 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Symbol table for EndBASIC compilation. + +use crate::bytecode::{MAX_GLOBAL_REGISTERS, Register}; +use std::fmt; + +use super::IdAssigner; + +/// The key of a symbol in the symbols table. +#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub struct SymbolKey(String); + +impl> From for SymbolKey { + fn from(value: R) -> Self { + Self(value.as_ref().to_ascii_uppercase()) + } +} + +impl fmt::Display for SymbolKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +pub(crate) struct Symtable { + globals: IdAssigner, + locals: Vec>, +} + +impl Default for Symtable { + fn default() -> Self { + Self { + globals: IdAssigner::new(0), + locals: vec![IdAssigner::new(MAX_GLOBAL_REGISTERS as usize)], + } + } +} + +impl Symtable { + pub(crate) fn global(&mut self, key: &SymbolKey) -> Register { + self.globals.get(key) + } + + pub(crate) fn local(&mut self, key: &SymbolKey) -> Register { + self.locals.last_mut().unwrap().get(key) + } + + pub(crate) fn nlocals(&self) -> usize { + self.locals.last().unwrap().len() + } +} diff --git a/core2/src/convert.rs b/core2/src/convert.rs new file mode 100644 index 00000000..2edf415c --- /dev/null +++ b/core2/src/convert.rs @@ -0,0 +1,80 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Efficient conversion utilities with diagnostics. + +use std::convert::TryFrom; + +/// Does a native cast from `Self` to `T`. +pub(crate) trait Cast { + fn cast(self) -> T; +} + +/// Implements `Cast` for two types, bidirectionally. +macro_rules! cast_impl { + ( $ty1:ty ) => { + impl Cast<$ty1> for $ty1 { + fn cast(self) -> $ty1 { + self as $ty1 + } + } + }; + + ( $ty1:ty, $ty2:ty ) => { + impl Cast<$ty1> for $ty2 { + fn cast(self) -> $ty1 { + self as $ty1 + } + } + + impl Cast<$ty2> for $ty1 { + fn cast(self) -> $ty2 { + self as $ty2 + } + } + }; +} + +cast_impl!(i16, u32); +cast_impl!(u16, u32); +cast_impl!(u8, u32); +cast_impl!(u8, usize); +cast_impl!(usize, u32); +cast_impl!(usize); + +/// Casts `value` to `T`. +/// +/// In debug mode, this performs the cast with range checks and panics if the conversion +/// is not possible. +/// +/// In release mode, this performs a native integer cast without checks. +pub(crate) fn cast(value: V) -> T +where + T: TryFrom, + >::Error: std::fmt::Debug, + V: Cast, +{ + if cfg!(debug_assertions) { T::try_from(value).unwrap() } else { Cast::::cast(value) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cast() { + assert_eq!(10u8, cast(10u32)); + } +} diff --git a/core2/src/lexer.rs b/core2/src/lexer.rs new file mode 100644 index 00000000..e358f612 --- /dev/null +++ b/core2/src/lexer.rs @@ -0,0 +1,1636 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Tokenizer for the EndBASIC language. + +use crate::ast::{ExprType, VarRef}; +use crate::reader::{CharReader, CharSpan, LineCol}; +use std::{fmt, io}; + +/// Result type for the public methods of this module. +type Result = std::result::Result; + +/// Collection of valid tokens. +/// +/// Of special interest are the `Eof` and `Bad` tokens, both of which denote exceptional +/// conditions and require special care. `Eof` indicates that there are no more tokens. +/// `Bad` indicates that a token was bad and contains the reason behind the problem, but the +/// stream remains valid for extraction of further tokens. +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub enum Token { + Eof, + Eol, + Bad(String), + + Boolean(bool), + Double(f64), + Integer(i32), + Text(String), + Symbol(VarRef), + + Label(String), + + Comma, + Semicolon, + LeftParen, + RightParen, + + Plus, + Minus, + Multiply, + Divide, + Modulo, + Exponent, + + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, + + And, + Not, + Or, + Xor, + + ShiftLeft, + ShiftRight, + + Case, + Data, + Do, + Else, + Elseif, + End, + Error, + Exit, + For, + Function, + Gosub, + Goto, + If, + Is, + Loop, + Next, + On, + Resume, + Return, + Select, + Sub, + Step, + Then, + To, + Until, + Wend, + While, + + Dim, + Shared, + As, + BooleanName, + DoubleName, + IntegerName, + TextName, +} + +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This implementation of Display returns the "canonical format" of a token. We could + // instead capture the original text that was in the input stream and store it in the + // TokenSpan and return that. However, most BASIC implementations make input canonical + // so this helps achieve that goal. + match self { + Token::Eof => write!(f, "<>"), + Token::Eol => write!(f, "<>"), + Token::Bad(s) => write!(f, "<<{}>>", s), + + Token::Boolean(false) => write!(f, "FALSE"), + Token::Boolean(true) => write!(f, "TRUE"), + Token::Double(d) => write!(f, "{}", d), + Token::Integer(i) => write!(f, "{}", i), + Token::Text(t) => write!(f, "{}", t), + Token::Symbol(vref) => write!(f, "{}", vref), + + Token::Label(l) => write!(f, "@{}", l), + + Token::Comma => write!(f, ","), + Token::Semicolon => write!(f, ";"), + Token::LeftParen => write!(f, "("), + Token::RightParen => write!(f, ")"), + + Token::Plus => write!(f, "+"), + Token::Minus => write!(f, "-"), + Token::Multiply => write!(f, "*"), + Token::Divide => write!(f, "/"), + Token::Modulo => write!(f, "MOD"), + Token::Exponent => write!(f, "^"), + + Token::Equal => write!(f, "="), + Token::NotEqual => write!(f, "<>"), + Token::Less => write!(f, "<"), + Token::LessEqual => write!(f, "<="), + Token::Greater => write!(f, ">"), + Token::GreaterEqual => write!(f, ">="), + + Token::And => write!(f, "AND"), + Token::Not => write!(f, "NOT"), + Token::Or => write!(f, "OR"), + Token::Xor => write!(f, "XOR"), + + Token::ShiftLeft => write!(f, "<<"), + Token::ShiftRight => write!(f, ">>"), + + Token::Case => write!(f, "CASE"), + Token::Data => write!(f, "DATA"), + Token::Do => write!(f, "DO"), + Token::Else => write!(f, "ELSE"), + Token::Elseif => write!(f, "ELSEIF"), + Token::End => write!(f, "END"), + Token::Error => write!(f, "ERROR"), + Token::Exit => write!(f, "EXIT"), + Token::For => write!(f, "FOR"), + Token::Function => write!(f, "FUNCTION"), + Token::Gosub => write!(f, "GOSUB"), + Token::Goto => write!(f, "GOTO"), + Token::If => write!(f, "IF"), + Token::Is => write!(f, "IS"), + Token::Loop => write!(f, "LOOP"), + Token::Next => write!(f, "NEXT"), + Token::On => write!(f, "ON"), + Token::Resume => write!(f, "RESUME"), + Token::Return => write!(f, "RETURN"), + Token::Select => write!(f, "SELECT"), + Token::Sub => write!(f, "SUB"), + Token::Step => write!(f, "STEP"), + Token::Then => write!(f, "THEN"), + Token::To => write!(f, "TO"), + Token::Until => write!(f, "UNTIL"), + Token::Wend => write!(f, "WEND"), + Token::While => write!(f, "WHILE"), + + Token::Dim => write!(f, "DIM"), + Token::Shared => write!(f, "SHARED"), + Token::As => write!(f, "AS"), + Token::BooleanName => write!(f, "BOOLEAN"), + Token::DoubleName => write!(f, "DOUBLE"), + Token::IntegerName => write!(f, "INTEGER"), + Token::TextName => write!(f, "STRING"), + } + } +} + +/// Extra operations to test properties of a `char` based on the language semantics. +trait CharOps { + /// Returns true if the current character should be considered as finishing a previous token. + fn is_separator(&self) -> bool; + + /// Returns true if the character is a space. + /// + /// Use this instead of `is_whitespace`, which accounts for newlines but we need to handle + /// those explicitly. + fn is_space(&self) -> bool; + + /// Returns true if the character can be part of an identifier. + fn is_word(&self) -> bool; +} + +impl CharOps for char { + fn is_separator(&self) -> bool { + match *self { + '\n' | ':' | '(' | ')' | '\'' | '=' | '<' | '>' | ';' | ',' | '+' | '-' | '*' | '/' + | '^' => true, + ch => ch.is_space(), + } + } + + fn is_space(&self) -> bool { + // TODO(jmmv): This is probably not correct regarding UTF-8 when comparing this function to + // the `is_whitespace` builtin. Figure out if that's true and what to do about it. + matches!(*self, ' ' | '\t' | '\r') + } + + fn is_word(&self) -> bool { + match *self { + '_' => true, + ch => ch.is_alphanumeric(), + } + } +} + +/// Container for a token and its context. +/// +/// Note that the "context" is not truly available for some tokens such as `Token::Eof`, but we can +/// synthesize one for simplicity. Otherwise, we would need to extend the `Token` enum so that +/// every possible token contains extra fields, and that would be too complex. +#[cfg_attr(test, derive(PartialEq))] +pub struct TokenSpan { + /// The token itself. + pub(crate) token: Token, + + /// Start position of the token. + pub(crate) pos: LineCol, + + /// Length of the token in characters. + #[allow(unused)] // TODO(jmmv): Use this in the parser. + length: usize, +} + +impl TokenSpan { + /// Creates a new `TokenSpan` from its parts. + fn new(token: Token, pos: LineCol, length: usize) -> Self { + Self { token, pos, length } + } +} + +/// Iterator over the tokens of the language. +pub struct Lexer<'a> { + /// Peekable iterator over the characters to scan. + input: CharReader<'a>, +} + +impl<'a> Lexer<'a> { + /// Creates a new lexer from the given readable. + pub fn from(input: &'a mut dyn io::Read) -> Self { + Self { input: CharReader::from(input) } + } + + /// Handles an `input.next()` call that returned an unexpected character. + /// + /// This returns a `Token::Bad` with the provided `msg` and skips characters in the input + /// stream until a field separator is found. + fn handle_bad_read>( + &mut self, + msg: S, + first_pos: LineCol, + ) -> io::Result { + let mut len = 1; + loop { + match self.input.peek() { + Some(Ok(ch_span)) if ch_span.ch.is_separator() => break, + Some(Ok(_)) => { + self.input.next().unwrap()?; + len += 1; + } + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + Ok(TokenSpan::new(Token::Bad(msg.into()), first_pos, len)) + } + + /// Consumes the number at the current position, whose first digit is `first`. + fn consume_number(&mut self, first: CharSpan) -> io::Result { + let mut s = String::new(); + let mut found_dot = false; + s.push(first.ch); + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + '.' => { + if found_dot { + self.input.next().unwrap()?; + return self + .handle_bad_read("Too many dots in numeric literal", first.pos); + } + s.push(self.input.next().unwrap()?.ch); + found_dot = true; + } + ch if ch.is_ascii_digit() => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + ch => { + self.input.next().unwrap()?; + let msg = format!("Unexpected character in numeric literal: {}", ch); + return self.handle_bad_read(msg, first.pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + if found_dot { + if s.ends_with('.') { + // TODO(jmmv): Reconsider supporting double literals with a . that is not prefixed + // by a number or not followed by a number. For now, mimic the error we get when + // we encounter a dot not prefixed by a number. + return self.handle_bad_read("Unknown character: .", first.pos); + } + match s.parse::() { + Ok(d) => Ok(TokenSpan::new(Token::Double(d), first.pos, s.len())), + Err(e) => self.handle_bad_read(format!("Bad double {}: {}", s, e), first.pos), + } + } else { + match s.parse::() { + Ok(i) => Ok(TokenSpan::new(Token::Integer(i), first.pos, s.len())), + Err(e) => self.handle_bad_read(format!("Bad integer {}: {}", s, e), first.pos), + } + } + } + + /// Consumes the integer at the current position, whose first digit is `first` and which is + /// expected to be expressed in the given `base`. `prefix_len` indicates how many characters + /// were already consumed for this token, without counting `first`. + fn consume_integer( + &mut self, + base: u8, + pos: LineCol, + prefix_len: usize, + ) -> io::Result { + let mut s = String::new(); + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + '.' => { + self.input.next().unwrap()?; + return self + .handle_bad_read("Numbers in base syntax must be integers", pos); + } + ch if ch.is_ascii_digit() => s.push(self.input.next().unwrap()?.ch), + 'a'..='f' | 'A'..='F' => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + ch => { + self.input.next().unwrap()?; + let msg = format!("Unexpected character in numeric literal: {}", ch); + return self.handle_bad_read(msg, pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + if s.is_empty() { + return self.handle_bad_read("No digits in integer literal", pos); + } + + match u32::from_str_radix(&s, u32::from(base)) { + Ok(i) => Ok(TokenSpan::new(Token::Integer(i as i32), pos, s.len() + prefix_len)), + Err(e) => self.handle_bad_read(format!("Bad integer {}: {}", s, e), pos), + } + } + + /// Consumes the integer at the current position `pos`. + fn consume_integer_with_base(&mut self, pos: LineCol) -> io::Result { + let mut prefix_len = 1; // Count '&'. + + let base = match self.input.peek() { + Some(Ok(ch_span)) => { + let base = match ch_span.ch { + 'b' | 'B' => 2, + 'd' | 'D' => 10, + 'o' | 'O' => 8, + 'x' | 'X' => 16, + ch if ch.is_separator() => { + return self.handle_bad_read("Missing base in integer literal", pos); + } + _ => { + let ch_span = self.input.next().unwrap()?; + return self.handle_bad_read( + format!("Unknown base {} in integer literal", ch_span.ch), + pos, + ); + } + }; + self.input.next().unwrap()?; + base + } + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => { + return self.handle_bad_read("Incomplete integer due to EOF", pos); + } + }; + prefix_len += 1; // Count the base. + + match self.input.peek() { + Some(Ok(ch_span)) if ch_span.ch == '_' => { + self.input.next().unwrap().unwrap(); + prefix_len += 1; // Count the '_'. + } + Some(Ok(_ch_span)) => (), + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => return self.handle_bad_read("Incomplete integer due to EOF", pos), + } + + self.consume_integer(base, pos, prefix_len) + } + + /// Consumes the operator at the current position, whose first character is `first`. + fn consume_operator(&mut self, first: CharSpan) -> io::Result { + match (first.ch, self.input.peek()) { + (_, Some(Err(_))) => Err(self.input.next().unwrap().unwrap_err()), + + ('<', Some(Ok(ch_span))) if ch_span.ch == '>' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::NotEqual, first.pos, 2)) + } + + ('<', Some(Ok(ch_span))) if ch_span.ch == '=' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::LessEqual, first.pos, 2)) + } + ('<', Some(Ok(ch_span))) if ch_span.ch == '<' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::ShiftLeft, first.pos, 2)) + } + ('<', _) => Ok(TokenSpan::new(Token::Less, first.pos, 1)), + + ('>', Some(Ok(ch_span))) if ch_span.ch == '=' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::GreaterEqual, first.pos, 2)) + } + ('>', Some(Ok(ch_span))) if ch_span.ch == '>' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::ShiftRight, first.pos, 2)) + } + ('>', _) => Ok(TokenSpan::new(Token::Greater, first.pos, 1)), + + (_, _) => panic!("Should not have been called"), + } + } + + /// Consumes the symbol or keyword at the current position, whose first letter is `first`. + /// + /// The symbol may be a bare name, but it may also contain an optional type annotation. + fn consume_symbol(&mut self, first: CharSpan) -> io::Result { + let mut s = String::new(); + s.push(first.ch); + let mut vtype = None; + let mut token_len = 0; + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + ch if ch.is_word() => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + '?' => { + vtype = Some(ExprType::Boolean); + self.input.next().unwrap()?; + token_len += 1; + break; + } + '#' => { + vtype = Some(ExprType::Double); + self.input.next().unwrap()?; + token_len += 1; + break; + } + '%' => { + vtype = Some(ExprType::Integer); + self.input.next().unwrap()?; + token_len += 1; + break; + } + '$' => { + vtype = Some(ExprType::Text); + self.input.next().unwrap()?; + token_len += 1; + break; + } + ch => { + self.input.next().unwrap()?; + let msg = format!("Unexpected character in symbol: {}", ch); + return self.handle_bad_read(msg, first.pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + debug_assert!(token_len <= 1); + + token_len += s.len(); + let token = match s.to_uppercase().as_str() { + "AND" => Token::And, + "AS" => Token::As, + "BOOLEAN" => Token::BooleanName, + "CASE" => Token::Case, + "DATA" => Token::Data, + "DIM" => Token::Dim, + "DO" => Token::Do, + "DOUBLE" => Token::DoubleName, + "ELSE" => Token::Else, + "ELSEIF" => Token::Elseif, + "END" => Token::End, + "ERROR" => Token::Error, + "EXIT" => Token::Exit, + "FALSE" => Token::Boolean(false), + "FOR" => Token::For, + "FUNCTION" => Token::Function, + "GOSUB" => Token::Gosub, + "GOTO" => Token::Goto, + "IF" => Token::If, + "IS" => Token::Is, + "INTEGER" => Token::IntegerName, + "LOOP" => Token::Loop, + "MOD" => Token::Modulo, + "NEXT" => Token::Next, + "NOT" => Token::Not, + "ON" => Token::On, + "OR" => Token::Or, + "REM" => return self.consume_rest_of_line(), + "RESUME" => Token::Resume, + "RETURN" => Token::Return, + "SELECT" => Token::Select, + "SHARED" => Token::Shared, + "STEP" => Token::Step, + "STRING" => Token::TextName, + "SUB" => Token::Sub, + "THEN" => Token::Then, + "TO" => Token::To, + "TRUE" => Token::Boolean(true), + "UNTIL" => Token::Until, + "WEND" => Token::Wend, + "WHILE" => Token::While, + "XOR" => Token::Xor, + _ => Token::Symbol(VarRef::new(s, vtype)), + }; + Ok(TokenSpan::new(token, first.pos, token_len)) + } + + /// Consumes the string at the current position, which was has to end with the same opening + /// character as specified by `delim`. + /// + /// This handles quoted characters within the string. + fn consume_text(&mut self, delim: CharSpan) -> io::Result { + let mut s = String::new(); + let mut escaping = false; + loop { + match self.input.peek() { + Some(Ok(ch_span)) => { + if escaping { + s.push(self.input.next().unwrap()?.ch); + escaping = false; + } else if ch_span.ch == '\\' { + self.input.next().unwrap()?; + escaping = true; + } else if ch_span.ch == delim.ch { + self.input.next().unwrap()?; + break; + } else { + s.push(self.input.next().unwrap()?.ch); + } + } + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => { + return self.handle_bad_read( + format!("Incomplete string due to EOF: {}", s), + delim.pos, + ); + } + } + } + let token_len = s.len() + 2; + Ok(TokenSpan::new(Token::Text(s), delim.pos, token_len)) + } + + /// Consumes the label definition at the current position. + fn consume_label(&mut self, first: CharSpan) -> io::Result { + let mut s = String::new(); + + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + ch if ch.is_word() && !ch.is_numeric() => s.push(self.input.next().unwrap()?.ch), + _ch => (), + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => (), + } + if s.is_empty() { + return Ok(TokenSpan::new(Token::Bad("Empty label name".to_owned()), first.pos, 1)); + } + + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + ch if ch.is_word() => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + ch => { + let msg = format!("Unexpected character in label: {}", ch); + return self.handle_bad_read(msg, first.pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + + let token_len = s.len() + 1; + Ok(TokenSpan::new(Token::Label(s), first.pos, token_len)) + } + + /// Consumes the remainder of the line and returns the token that was encountered at the end + /// (which may be EOF or end of line). + fn consume_rest_of_line(&mut self) -> io::Result { + loop { + match self.input.next() { + None => { + let last_pos = self.input.next_pos(); + return Ok(TokenSpan::new(Token::Eof, last_pos, 0)); + } + Some(Ok(ch_span)) if ch_span.ch == '\n' => { + return Ok(TokenSpan::new(Token::Eol, ch_span.pos, 1)) + } + Some(Err(e)) => return Err(e), + Some(Ok(_)) => (), + } + } + } + + /// Skips whitespace until it finds the beginning of the next token, and returns its first + /// character. + fn advance_and_read_next(&mut self) -> io::Result> { + loop { + match self.input.next() { + Some(Ok(ch_span)) if ch_span.ch.is_space() => (), + Some(Ok(ch_span)) => return Ok(Some(ch_span)), + Some(Err(e)) => return Err(e), + None => return Ok(None), + } + } + } + + /// Reads the next token from the input stream. + /// + /// Note that this returns errors only on fatal I/O conditions. EOF and malformed tokens are + /// both returned as the special token types `Token::Eof` and `Token::Bad` respectively. + pub fn read(&mut self) -> io::Result { + let ch_span = self.advance_and_read_next()?; + if ch_span.is_none() { + let last_pos = self.input.next_pos(); + return Ok(TokenSpan::new(Token::Eof, last_pos, 0)); + } + let ch_span = ch_span.unwrap(); + match ch_span.ch { + '\n' | ':' => Ok(TokenSpan::new(Token::Eol, ch_span.pos, 1)), + '\'' => self.consume_rest_of_line(), + + '"' => self.consume_text(ch_span), + + ';' => Ok(TokenSpan::new(Token::Semicolon, ch_span.pos, 1)), + ',' => Ok(TokenSpan::new(Token::Comma, ch_span.pos, 1)), + + '(' => Ok(TokenSpan::new(Token::LeftParen, ch_span.pos, 1)), + ')' => Ok(TokenSpan::new(Token::RightParen, ch_span.pos, 1)), + + '+' => Ok(TokenSpan::new(Token::Plus, ch_span.pos, 1)), + '-' => Ok(TokenSpan::new(Token::Minus, ch_span.pos, 1)), + '*' => Ok(TokenSpan::new(Token::Multiply, ch_span.pos, 1)), + '/' => Ok(TokenSpan::new(Token::Divide, ch_span.pos, 1)), + '^' => Ok(TokenSpan::new(Token::Exponent, ch_span.pos, 1)), + + '=' => Ok(TokenSpan::new(Token::Equal, ch_span.pos, 1)), + '<' | '>' => self.consume_operator(ch_span), + + '@' => self.consume_label(ch_span), + + '&' => self.consume_integer_with_base(ch_span.pos), + + ch if ch.is_ascii_digit() => self.consume_number(ch_span), + ch if ch.is_word() => self.consume_symbol(ch_span), + ch => self.handle_bad_read(format!("Unknown character: {}", ch), ch_span.pos), + } + } + + /// Returns a peekable adaptor for this lexer. + pub fn peekable(self) -> PeekableLexer<'a> { + PeekableLexer { lexer: self, peeked: None } + } +} + +/// Wraps a `Lexer` and offers peeking abilities. +/// +/// Ideally, the `Lexer` would be an `Iterator` which would give us access to the standard +/// `Peekable` interface, but the ergonomics of that when dealing with a `Fallible` are less than +/// optimal. Hence we implement our own. +pub struct PeekableLexer<'a> { + /// The wrapped lexer instance. + lexer: Lexer<'a>, + + /// If not none, contains the character read by `peek`, which will be consumed by the next call + /// to `read` or `consume_peeked`. + peeked: Option, +} + +impl PeekableLexer<'_> { + /// Reads the previously-peeked token. + /// + /// Because `peek` reports read errors, this assumes that the caller already handled those + /// errors and is thus not going to call this when an error is present. + pub fn consume_peeked(&mut self) -> TokenSpan { + assert!(self.peeked.is_some()); + self.peeked.take().unwrap() + } + + /// Peeks the upcoming token. + /// + /// It is OK to call this function several times on the same token before extracting it from + /// the lexer. + pub fn peek(&mut self) -> Result<&TokenSpan> { + if self.peeked.is_none() { + let span = self.read()?; + self.peeked.replace(span); + } + Ok(self.peeked.as_ref().unwrap()) + } + + /// Reads the next token. + /// + /// If the next token is invalid and results in a read error, the stream will remain valid and + /// further tokens can be obtained with subsequent calls. + pub fn read(&mut self) -> Result { + match self.peeked.take() { + Some(t) => Ok(t), + None => match self.lexer.read() { + Ok(span) => Ok(span), + Err(e) => Err((self.lexer.input.next_pos(), e)), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt; + + /// Syntactic sugar to instantiate a `TokenSpan` for testing. + fn ts(token: Token, line: usize, col: usize, length: usize) -> TokenSpan { + TokenSpan::new(token, LineCol { line, col }, length) + } + + impl fmt::Debug for TokenSpan { + /// Mimic the way we write the tests with the `ts` helper in `TokenSpan` dumps. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ts(Token::{:?}, {}, {}, {})", + self.token, self.pos.line, self.pos.col, self.length + ) + } + } + + /// Runs the lexer on the given `input` and expects the returned tokens to match + /// `exp_token_spans`. + fn do_ok_test(input: &str, exp_token_spans: &[TokenSpan]) { + let mut input = input.as_bytes(); + let mut lexer = Lexer::from(&mut input); + + let mut token_spans: Vec = vec![]; + let mut eof = false; + while !eof { + let token_span = lexer.read().expect("Lexing failed"); + eof = token_span.token == Token::Eof; + token_spans.push(token_span); + } + + assert_eq!(exp_token_spans, token_spans.as_slice()); + } + + #[test] + fn test_empty() { + let mut input = b"".as_ref(); + let mut lexer = Lexer::from(&mut input); + assert_eq!(Token::Eof, lexer.read().unwrap().token); + assert_eq!(Token::Eof, lexer.read().unwrap().token); + } + + #[test] + fn test_read_past_eof() { + do_ok_test("", &[ts(Token::Eof, 1, 1, 0)]); + } + + #[test] + fn test_whitespace_only() { + do_ok_test(" \t ", &[ts(Token::Eof, 1, 11, 0)]); + } + + #[test] + fn test_multiple_lines() { + do_ok_test( + " \n \t \n ", + &[ts(Token::Eol, 1, 4, 1), ts(Token::Eol, 2, 12, 1), ts(Token::Eof, 3, 3, 0)], + ); + do_ok_test( + " : \t : ", + &[ts(Token::Eol, 1, 4, 1), ts(Token::Eol, 1, 12, 1), ts(Token::Eof, 1, 15, 0)], + ); + } + + #[test] + fn test_tabs() { + do_ok_test("\t33", &[ts(Token::Integer(33), 1, 9, 2), ts(Token::Eof, 1, 11, 0)]); + do_ok_test( + "1234567\t8", + &[ + ts(Token::Integer(1234567), 1, 1, 7), + ts(Token::Integer(8), 1, 9, 1), + ts(Token::Eof, 1, 10, 0), + ], + ); + } + + /// Syntactic sugar to instantiate a `VarRef` without an explicit type annotation. + fn new_auto_symbol(name: &str) -> Token { + Token::Symbol(VarRef::new(name, None)) + } + + #[test] + fn test_some_tokens() { + do_ok_test( + "123 45 \n 6 3.012 abc a38z: a=3 with_underscores_1=_2", + &[ + ts(Token::Integer(123), 1, 1, 3), + ts(Token::Integer(45), 1, 5, 2), + ts(Token::Eol, 1, 8, 1), + ts(Token::Integer(6), 2, 2, 1), + ts(Token::Double(3.012), 2, 4, 5), + ts(new_auto_symbol("abc"), 2, 10, 3), + ts(new_auto_symbol("a38z"), 2, 14, 4), + ts(Token::Eol, 2, 18, 1), + ts(new_auto_symbol("a"), 2, 20, 1), + ts(Token::Equal, 2, 21, 1), + ts(Token::Integer(3), 2, 22, 1), + ts(new_auto_symbol("with_underscores_1"), 2, 24, 18), + ts(Token::Equal, 2, 42, 1), + ts(new_auto_symbol("_2"), 2, 43, 2), + ts(Token::Eof, 2, 45, 0), + ], + ); + } + + #[test] + fn test_boolean_literals() { + do_ok_test( + "true TRUE yes YES y false FALSE no NO n", + &[ + ts(Token::Boolean(true), 1, 1, 4), + ts(Token::Boolean(true), 1, 6, 4), + ts(new_auto_symbol("yes"), 1, 11, 3), + ts(new_auto_symbol("YES"), 1, 15, 3), + ts(new_auto_symbol("y"), 1, 19, 1), + ts(Token::Boolean(false), 1, 21, 5), + ts(Token::Boolean(false), 1, 27, 5), + ts(new_auto_symbol("no"), 1, 33, 2), + ts(new_auto_symbol("NO"), 1, 36, 2), + ts(new_auto_symbol("n"), 1, 39, 1), + ts(Token::Eof, 1, 40, 0), + ], + ); + } + + #[test] + fn test_integer_literals() { + do_ok_test( + "&b10 &B_10 &D10 &d_10 &o_10 &O_10 &X10 &x_10 &xabcdef &x0ABCDEF0 &x7a1", + &[ + ts(Token::Integer(2), 1, 1, 4), + ts(Token::Integer(2), 1, 6, 5), + ts(Token::Integer(10), 1, 12, 4), + ts(Token::Integer(10), 1, 17, 5), + ts(Token::Integer(8), 1, 23, 5), + ts(Token::Integer(8), 1, 29, 5), + ts(Token::Integer(16), 1, 35, 4), + ts(Token::Integer(16), 1, 40, 5), + ts(Token::Integer(11259375), 1, 46, 8), + ts(Token::Integer(180150000), 1, 55, 10), + ts(Token::Integer(1953), 1, 66, 5), + ts(Token::Eof, 1, 71, 0), + ], + ); + + do_ok_test( + "&b11111111111111111111111111111111 &xf0000000 &xffffffff", + &[ + ts(Token::Integer(-1), 1, 1, 34), + ts(Token::Integer(-268435456), 1, 36, 10), + ts(Token::Integer(-1), 1, 47, 10), + ts(Token::Eof, 1, 57, 0), + ], + ); + + do_ok_test( + "& &_ &__ &i10 &i_10 &d &d10.1 &b2 &da &o8 &xg", + &[ + ts(Token::Bad("Missing base in integer literal".to_owned()), 1, 1, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 3, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 6, 2), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 10, 3), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 15, 4), + ts(Token::Bad("No digits in integer literal".to_owned()), 1, 21, 1), + ts(Token::Bad("Numbers in base syntax must be integers".to_owned()), 1, 24, 2), + ts(Token::Bad("Bad integer 2: invalid digit found in string".to_owned()), 1, 31, 1), + ts(Token::Bad("Bad integer a: invalid digit found in string".to_owned()), 1, 35, 1), + ts(Token::Bad("Bad integer 8: invalid digit found in string".to_owned()), 1, 39, 1), + ts(Token::Bad("Unexpected character in numeric literal: g".to_owned()), 1, 43, 1), + ts(Token::Eof, 1, 46, 0), + ], + ); + + do_ok_test( + ">&< >&_< >&__< >&i10< >&i_10< >&d< >&d10.1<", + &[ + ts(Token::Greater, 1, 1, 1), + ts(Token::Bad("Missing base in integer literal".to_owned()), 1, 2, 1), + ts(Token::Less, 1, 3, 1), + // - + ts(Token::Greater, 1, 5, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 6, 1), + ts(Token::Less, 1, 8, 1), + // - + ts(Token::Greater, 1, 10, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 11, 2), + ts(Token::Less, 1, 14, 1), + // - + ts(Token::Greater, 1, 16, 1), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 17, 3), + ts(Token::Less, 1, 21, 1), + // - + ts(Token::Greater, 1, 23, 1), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 24, 4), + ts(Token::Less, 1, 29, 1), + // - + ts(Token::Greater, 1, 31, 1), + ts(Token::Bad("No digits in integer literal".to_owned()), 1, 32, 1), + ts(Token::Less, 1, 34, 1), + // - + ts(Token::Greater, 1, 36, 1), + ts(Token::Bad("Numbers in base syntax must be integers".to_owned()), 1, 37, 2), + ts(Token::Less, 1, 43, 1), + // - + ts(Token::Eof, 1, 44, 0), + ], + ); + } + + #[test] + fn test_utf8() { + do_ok_test( + "가 나=7 a다b \"라 마\"", + &[ + ts(new_auto_symbol("가"), 1, 1, 3), + ts(new_auto_symbol("나"), 1, 3, 3), + ts(Token::Equal, 1, 4, 1), + ts(Token::Integer(7), 1, 5, 1), + ts(new_auto_symbol("a다b"), 1, 7, 5), + ts(Token::Text("라 마".to_owned()), 1, 11, 9), + ts(Token::Eof, 1, 16, 0), + ], + ); + } + + #[test] + fn test_remarks() { + do_ok_test( + "REM This is a comment\nNOT 'This is another comment\n", + &[ + ts(Token::Eol, 1, 22, 1), + ts(Token::Not, 2, 1, 3), + ts(Token::Eol, 2, 29, 1), + ts(Token::Eof, 3, 1, 0), + ], + ); + + do_ok_test( + "REM This is a comment: and the colon doesn't yield Eol\nNOT 'Another: comment\n", + &[ + ts(Token::Eol, 1, 55, 1), + ts(Token::Not, 2, 1, 3), + ts(Token::Eol, 2, 22, 1), + ts(Token::Eof, 3, 1, 0), + ], + ); + } + + #[test] + fn test_var_types() { + do_ok_test( + "a b? d# i% s$", + &[ + ts(new_auto_symbol("a"), 1, 1, 1), + ts(Token::Symbol(VarRef::new("b", Some(ExprType::Boolean))), 1, 3, 2), + ts(Token::Symbol(VarRef::new("d", Some(ExprType::Double))), 1, 6, 2), + ts(Token::Symbol(VarRef::new("i", Some(ExprType::Integer))), 1, 9, 2), + ts(Token::Symbol(VarRef::new("s", Some(ExprType::Text))), 1, 12, 2), + ts(Token::Eof, 1, 14, 0), + ], + ); + } + + #[test] + fn test_strings() { + do_ok_test( + " \"this is a string\" 3", + &[ + ts(Token::Text("this is a string".to_owned()), 1, 2, 18), + ts(Token::Integer(3), 1, 22, 1), + ts(Token::Eof, 1, 23, 0), + ], + ); + + do_ok_test( + " \"this is a string with ; special : characters in it\"", + &[ + ts( + Token::Text("this is a string with ; special : characters in it".to_owned()), + 1, + 2, + 52, + ), + ts(Token::Eof, 1, 54, 0), + ], + ); + + do_ok_test( + "\"this \\\"is escaped\\\" \\\\ \\a\" 1", + &[ + ts(Token::Text("this \"is escaped\" \\ a".to_owned()), 1, 1, 23), + ts(Token::Integer(1), 1, 29, 1), + ts(Token::Eof, 1, 30, 0), + ], + ); + } + + #[test] + fn test_data() { + do_ok_test("DATA", &[ts(Token::Data, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + + do_ok_test("data", &[ts(Token::Data, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + + // Common BASIC interprets things like "2 + foo" as a single string but we interpret + // separate tokens. "Fixing" this to read data in the same way requires entering a + // separate lexing mode just for DATA statements, which is not very interesting. We can + // ask for strings to always be double-quoted. + do_ok_test( + "DATA 2 + foo", + &[ + ts(Token::Data, 1, 1, 4), + ts(Token::Integer(2), 1, 6, 1), + ts(Token::Plus, 1, 8, 1), + ts(new_auto_symbol("foo"), 1, 10, 3), + ts(Token::Eof, 1, 13, 0), + ], + ); + } + + #[test] + fn test_dim() { + do_ok_test( + "DIM SHARED AS", + &[ + ts(Token::Dim, 1, 1, 3), + ts(Token::Shared, 1, 5, 6), + ts(Token::As, 1, 12, 2), + ts(Token::Eof, 1, 14, 0), + ], + ); + do_ok_test( + "BOOLEAN DOUBLE INTEGER STRING", + &[ + ts(Token::BooleanName, 1, 1, 7), + ts(Token::DoubleName, 1, 9, 6), + ts(Token::IntegerName, 1, 16, 7), + ts(Token::TextName, 1, 24, 6), + ts(Token::Eof, 1, 30, 0), + ], + ); + + do_ok_test( + "dim shared as", + &[ + ts(Token::Dim, 1, 1, 3), + ts(Token::Shared, 1, 5, 6), + ts(Token::As, 1, 12, 2), + ts(Token::Eof, 1, 14, 0), + ], + ); + do_ok_test( + "boolean double integer string", + &[ + ts(Token::BooleanName, 1, 1, 7), + ts(Token::DoubleName, 1, 9, 6), + ts(Token::IntegerName, 1, 16, 7), + ts(Token::TextName, 1, 24, 6), + ts(Token::Eof, 1, 30, 0), + ], + ); + } + + #[test] + fn test_do() { + do_ok_test( + "DO UNTIL WHILE EXIT LOOP", + &[ + ts(Token::Do, 1, 1, 2), + ts(Token::Until, 1, 4, 5), + ts(Token::While, 1, 10, 5), + ts(Token::Exit, 1, 16, 4), + ts(Token::Loop, 1, 21, 4), + ts(Token::Eof, 1, 25, 0), + ], + ); + + do_ok_test( + "do until while exit loop", + &[ + ts(Token::Do, 1, 1, 2), + ts(Token::Until, 1, 4, 5), + ts(Token::While, 1, 10, 5), + ts(Token::Exit, 1, 16, 4), + ts(Token::Loop, 1, 21, 4), + ts(Token::Eof, 1, 25, 0), + ], + ); + } + + #[test] + fn test_if() { + do_ok_test( + "IF THEN ELSEIF ELSE END IF", + &[ + ts(Token::If, 1, 1, 2), + ts(Token::Then, 1, 4, 4), + ts(Token::Elseif, 1, 9, 6), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::If, 1, 25, 2), + ts(Token::Eof, 1, 27, 0), + ], + ); + + do_ok_test( + "if then elseif else end if", + &[ + ts(Token::If, 1, 1, 2), + ts(Token::Then, 1, 4, 4), + ts(Token::Elseif, 1, 9, 6), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::If, 1, 25, 2), + ts(Token::Eof, 1, 27, 0), + ], + ); + } + + #[test] + fn test_for() { + do_ok_test( + "FOR TO STEP NEXT", + &[ + ts(Token::For, 1, 1, 3), + ts(Token::To, 1, 5, 2), + ts(Token::Step, 1, 8, 4), + ts(Token::Next, 1, 13, 4), + ts(Token::Eof, 1, 17, 0), + ], + ); + + do_ok_test( + "for to step next", + &[ + ts(Token::For, 1, 1, 3), + ts(Token::To, 1, 5, 2), + ts(Token::Step, 1, 8, 4), + ts(Token::Next, 1, 13, 4), + ts(Token::Eof, 1, 17, 0), + ], + ); + } + + #[test] + fn test_function() { + do_ok_test( + "FUNCTION FOO END FUNCTION", + &[ + ts(Token::Function, 1, 1, 8), + ts(Token::Symbol(VarRef::new("FOO", None)), 1, 10, 3), + ts(Token::End, 1, 14, 3), + ts(Token::Function, 1, 18, 8), + ts(Token::Eof, 1, 26, 0), + ], + ); + + do_ok_test( + "function foo end function", + &[ + ts(Token::Function, 1, 1, 8), + ts(Token::Symbol(VarRef::new("foo", None)), 1, 10, 3), + ts(Token::End, 1, 14, 3), + ts(Token::Function, 1, 18, 8), + ts(Token::Eof, 1, 26, 0), + ], + ); + } + + #[test] + fn test_gosub() { + do_ok_test("GOSUB", &[ts(Token::Gosub, 1, 1, 5), ts(Token::Eof, 1, 6, 0)]); + + do_ok_test("gosub", &[ts(Token::Gosub, 1, 1, 5), ts(Token::Eof, 1, 6, 0)]); + } + + #[test] + fn test_goto() { + do_ok_test("GOTO", &[ts(Token::Goto, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + + do_ok_test("goto", &[ts(Token::Goto, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + } + + #[test] + fn test_label() { + do_ok_test( + "@Foo123 @a @Z @_ok", + &[ + ts(Token::Label("Foo123".to_owned()), 1, 1, 7), + ts(Token::Label("a".to_owned()), 1, 9, 2), + ts(Token::Label("Z".to_owned()), 1, 12, 2), + ts(Token::Label("_ok".to_owned()), 1, 15, 4), + ts(Token::Eof, 1, 19, 0), + ], + ); + } + + #[test] + fn test_on_error() { + for s in ["ON ERROR GOTO @foo", "on error goto @foo"] { + do_ok_test( + s, + &[ + ts(Token::On, 1, 1, 2), + ts(Token::Error, 1, 4, 5), + ts(Token::Goto, 1, 10, 4), + ts(Token::Label("foo".to_owned()), 1, 15, 4), + ts(Token::Eof, 1, 19, 0), + ], + ); + } + + for s in ["ON ERROR RESUME NEXT", "on error resume next"] { + do_ok_test( + s, + &[ + ts(Token::On, 1, 1, 2), + ts(Token::Error, 1, 4, 5), + ts(Token::Resume, 1, 10, 6), + ts(Token::Next, 1, 17, 4), + ts(Token::Eof, 1, 21, 0), + ], + ); + } + } + + #[test] + fn test_return() { + do_ok_test("RETURN", &[ts(Token::Return, 1, 1, 6), ts(Token::Eof, 1, 7, 0)]); + + do_ok_test("return", &[ts(Token::Return, 1, 1, 6), ts(Token::Eof, 1, 7, 0)]); + } + + #[test] + fn test_select() { + do_ok_test( + "SELECT CASE IS ELSE END", + &[ + ts(Token::Select, 1, 1, 6), + ts(Token::Case, 1, 8, 4), + ts(Token::Is, 1, 13, 2), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::Eof, 1, 24, 0), + ], + ); + + do_ok_test( + "select case is else end", + &[ + ts(Token::Select, 1, 1, 6), + ts(Token::Case, 1, 8, 4), + ts(Token::Is, 1, 13, 2), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::Eof, 1, 24, 0), + ], + ); + } + + #[test] + fn test_sub() { + do_ok_test( + "SUB FOO END SUB", + &[ + ts(Token::Sub, 1, 1, 3), + ts(Token::Symbol(VarRef::new("FOO", None)), 1, 5, 3), + ts(Token::End, 1, 9, 3), + ts(Token::Sub, 1, 13, 3), + ts(Token::Eof, 1, 16, 0), + ], + ); + + do_ok_test( + "sub foo end sub", + &[ + ts(Token::Sub, 1, 1, 3), + ts(Token::Symbol(VarRef::new("foo", None)), 1, 5, 3), + ts(Token::End, 1, 9, 3), + ts(Token::Sub, 1, 13, 3), + ts(Token::Eof, 1, 16, 0), + ], + ); + } + + #[test] + fn test_while() { + do_ok_test( + "WHILE WEND", + &[ts(Token::While, 1, 1, 5), ts(Token::Wend, 1, 7, 4), ts(Token::Eof, 1, 11, 0)], + ); + + do_ok_test( + "while wend", + &[ts(Token::While, 1, 1, 5), ts(Token::Wend, 1, 7, 4), ts(Token::Eof, 1, 11, 0)], + ); + } + + /// Syntactic sugar to instantiate a test that verifies the parsing of a binary operator. + fn do_binary_operator_test(op: &str, t: Token) { + do_ok_test( + format!("a {} 2", op).as_ref(), + &[ + ts(new_auto_symbol("a"), 1, 1, 1), + ts(t, 1, 3, op.len()), + ts(Token::Integer(2), 1, 4 + op.len(), 1), + ts(Token::Eof, 1, 5 + op.len(), 0), + ], + ); + } + + /// Syntactic sugar to instantiate a test that verifies the parsing of a unary operator. + fn do_unary_operator_test(op: &str, t: Token) { + do_ok_test( + format!("{} 2", op).as_ref(), + &[ + ts(t, 1, 1, op.len()), + ts(Token::Integer(2), 1, 2 + op.len(), 1), + ts(Token::Eof, 1, 3 + op.len(), 0), + ], + ); + } + + #[test] + fn test_operator_relational_ops() { + do_binary_operator_test("=", Token::Equal); + do_binary_operator_test("<>", Token::NotEqual); + do_binary_operator_test("<", Token::Less); + do_binary_operator_test("<=", Token::LessEqual); + do_binary_operator_test(">", Token::Greater); + do_binary_operator_test(">=", Token::GreaterEqual); + } + + #[test] + fn test_operator_arithmetic_ops() { + do_binary_operator_test("+", Token::Plus); + do_binary_operator_test("-", Token::Minus); + do_binary_operator_test("*", Token::Multiply); + do_binary_operator_test("/", Token::Divide); + do_binary_operator_test("MOD", Token::Modulo); + do_binary_operator_test("mod", Token::Modulo); + do_binary_operator_test("^", Token::Exponent); + do_unary_operator_test("-", Token::Minus); + } + + #[test] + fn test_operator_logical_bitwise_ops() { + do_binary_operator_test("AND", Token::And); + do_binary_operator_test("OR", Token::Or); + do_binary_operator_test("XOR", Token::Xor); + do_unary_operator_test("NOT", Token::Not); + + do_binary_operator_test("<<", Token::ShiftLeft); + do_binary_operator_test(">>", Token::ShiftRight); + } + + #[test] + fn test_operator_no_spaces() { + do_ok_test( + "z=2 654<>a32 3.1<0.1 8^7", + &[ + ts(new_auto_symbol("z"), 1, 1, 1), + ts(Token::Equal, 1, 2, 1), + ts(Token::Integer(2), 1, 3, 1), + ts(Token::Integer(654), 1, 5, 3), + ts(Token::NotEqual, 1, 8, 2), + ts(new_auto_symbol("a32"), 1, 10, 3), + ts(Token::Double(3.1), 1, 14, 3), + ts(Token::Less, 1, 17, 1), + ts(Token::Double(0.1), 1, 18, 3), + ts(Token::Integer(8), 1, 22, 1), + ts(Token::Exponent, 1, 23, 1), + ts(Token::Integer(7), 1, 24, 1), + ts(Token::Eof, 1, 25, 0), + ], + ); + } + + #[test] + fn test_parenthesis() { + do_ok_test( + "(a) (\"foo\") (3)", + &[ + ts(Token::LeftParen, 1, 1, 1), + ts(new_auto_symbol("a"), 1, 2, 1), + ts(Token::RightParen, 1, 3, 1), + ts(Token::LeftParen, 1, 5, 1), + ts(Token::Text("foo".to_owned()), 1, 6, 5), + ts(Token::RightParen, 1, 11, 1), + ts(Token::LeftParen, 1, 13, 1), + ts(Token::Integer(3), 1, 14, 1), + ts(Token::RightParen, 1, 15, 1), + ts(Token::Eof, 1, 16, 0), + ], + ); + } + + #[test] + fn test_peekable_lexer() { + let mut input = b"a b 123".as_ref(); + let mut lexer = Lexer::from(&mut input).peekable(); + assert_eq!(new_auto_symbol("a"), lexer.peek().unwrap().token); + assert_eq!(new_auto_symbol("a"), lexer.peek().unwrap().token); + assert_eq!(new_auto_symbol("a"), lexer.read().unwrap().token); + assert_eq!(new_auto_symbol("b"), lexer.read().unwrap().token); + assert_eq!(Token::Integer(123), lexer.peek().unwrap().token); + assert_eq!(Token::Integer(123), lexer.read().unwrap().token); + assert_eq!(Token::Eof, lexer.peek().unwrap().token); + assert_eq!(Token::Eof, lexer.read().unwrap().token); + } + + #[test] + fn test_recoverable_errors() { + do_ok_test( + "0.1.28+5", + &[ + ts(Token::Bad("Too many dots in numeric literal".to_owned()), 1, 1, 3), + ts(Token::Plus, 1, 7, 1), + ts(Token::Integer(5), 1, 8, 1), + ts(Token::Eof, 1, 9, 0), + ], + ); + + do_ok_test( + "1 .3", + &[ + ts(Token::Integer(1), 1, 1, 1), + ts(Token::Bad("Unknown character: .".to_owned()), 1, 3, 2), + ts(Token::Eof, 1, 5, 0), + ], + ); + + do_ok_test( + "1 3. 2", + &[ + ts(Token::Integer(1), 1, 1, 1), + ts(Token::Bad("Unknown character: .".to_owned()), 1, 3, 1), + ts(Token::Integer(2), 1, 6, 1), + ts(Token::Eof, 1, 7, 0), + ], + ); + + do_ok_test( + "9999999999+5", + &[ + ts( + Token::Bad( + "Bad integer 9999999999: number too large to fit in target type".to_owned(), + ), + 1, + 1, + 1, + ), + ts(Token::Plus, 1, 11, 1), + ts(Token::Integer(5), 1, 12, 1), + ts(Token::Eof, 1, 13, 0), + ], + ); + + do_ok_test( + "\n3!2 1", + &[ + ts(Token::Eol, 1, 1, 1), + ts(Token::Bad("Unexpected character in numeric literal: !".to_owned()), 2, 1, 2), + ts(Token::Integer(1), 2, 5, 1), + ts(Token::Eof, 2, 6, 0), + ], + ); + + do_ok_test( + "a b|d 5", + &[ + ts(new_auto_symbol("a"), 1, 1, 1), + ts(Token::Bad("Unexpected character in symbol: |".to_owned()), 1, 3, 2), + ts(Token::Integer(5), 1, 7, 1), + ts(Token::Eof, 1, 8, 0), + ], + ); + + do_ok_test( + "( \"this is incomplete", + &[ + ts(Token::LeftParen, 1, 1, 1), + ts( + Token::Bad("Incomplete string due to EOF: this is incomplete".to_owned()), + 1, + 3, + 1, + ), + ts(Token::Eof, 1, 22, 0), + ], + ); + + do_ok_test( + "+ - ! * / MOD ^", + &[ + ts(Token::Plus, 1, 1, 1), + ts(Token::Minus, 1, 3, 1), + ts(Token::Bad("Unknown character: !".to_owned()), 1, 5, 1), + ts(Token::Multiply, 1, 7, 1), + ts(Token::Divide, 1, 9, 1), + ts(Token::Modulo, 1, 11, 3), + ts(Token::Exponent, 1, 15, 1), + ts(Token::Eof, 1, 16, 0), + ], + ); + + do_ok_test( + "@+", + &[ + ts(Token::Bad("Empty label name".to_owned()), 1, 1, 1), + ts(Token::Plus, 1, 2, 1), + ts(Token::Eof, 1, 3, 0), + ], + ); + + do_ok_test( + "@123", + &[ + ts(Token::Bad("Empty label name".to_owned()), 1, 1, 1), + ts(Token::Integer(123), 1, 2, 3), + ts(Token::Eof, 1, 5, 0), + ], + ); + } + + /// A reader that generates an error on the second read. + /// + /// Assumes that the buffered data in `good` is read in one go. + struct FaultyReader { + good: Option>, + } + + impl FaultyReader { + /// Creates a new faulty read with the given input data. + /// + /// `good` must be newline-terminated to prevent the caller from reading too much in one go. + fn new(good: &str) -> Self { + assert!(good.ends_with('\n')); + Self { good: Some(good.as_bytes().to_owned()) } + } + } + + impl io::Read for FaultyReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + // This assumes that the good data fits within one read operation of the lexer. + if let Some(good) = self.good.take() { + assert!(buf.len() > good.len()); + buf[0..good.len()].clone_from_slice(&good[..]); + Ok(good.len()) + } else { + Err(io::Error::from(io::ErrorKind::InvalidData)) + } + } + } + + #[test] + fn test_unrecoverable_io_error() { + let mut reader = FaultyReader::new("3 + 5\n"); + let mut lexer = Lexer::from(&mut reader); + + assert_eq!(Token::Integer(3), lexer.read().unwrap().token); + assert_eq!(Token::Plus, lexer.read().unwrap().token); + assert_eq!(Token::Integer(5), lexer.read().unwrap().token); + assert_eq!(Token::Eol, lexer.read().unwrap().token); + let e = lexer.read().unwrap_err(); + assert_eq!(io::ErrorKind::InvalidData, e.kind()); + let e = lexer.read().unwrap_err(); + assert_eq!(io::ErrorKind::Other, e.kind()); + } +} diff --git a/core2/src/lib.rs b/core2/src/lib.rs new file mode 100644 index 00000000..1b37e769 --- /dev/null +++ b/core2/src/lib.rs @@ -0,0 +1,30 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! The EndBASIC core language parser, compiler, and virtual machine. + +mod ast; +mod bytecode; +mod callable; +mod compiler; +mod convert; +mod lexer; +mod parser; +mod reader; +mod vm; + +pub use callable::{Callable, CallableMetadata, CallableMetadataBuilder, Scope}; +pub use compiler::{SymbolKey, compile}; +pub use vm::{Context, StopReason, Vm}; diff --git a/core2/src/parser.rs b/core2/src/parser.rs new file mode 100644 index 00000000..d72a6e4d --- /dev/null +++ b/core2/src/parser.rs @@ -0,0 +1,4850 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Statement and expression parser for the EndBASIC language. + +use crate::ast::*; +use crate::lexer::{Lexer, PeekableLexer, Token, TokenSpan}; +use crate::reader::LineCol; +use std::cmp::Ordering; +use std::io; + +/// Parser errors. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Bad syntax in the input program. + #[error("{}: {}", .0, .1)] + Bad(LineCol, String), + + /// I/O error while parsing the input program. + #[error("{0}: {1}")] + Io(LineCol, io::Error), +} + +impl From<(LineCol, io::Error)> for Error { + fn from(value: (LineCol, io::Error)) -> Self { + Self::Io(value.0, value.1) + } +} + +/// Result for parser return values. +pub type Result = std::result::Result; + +/// Transforms a `VarRef` into an unannotated name. +/// +/// This is only valid for references that have no annotations in them. +fn vref_to_unannotated_string(vref: VarRef, pos: LineCol) -> Result { + if vref.ref_type.is_some() { + return Err(Error::Bad(pos, format!("Type annotation not allowed in {}", vref))); + } + Ok(vref.name) +} + +/// Converts a collection of `ArgSpan`s passed to a function or array reference to a collection +/// of expressions with proper validation. +pub(crate) fn argspans_to_exprs(spans: Vec) -> Vec { + let nargs = spans.len(); + let mut exprs = Vec::with_capacity(spans.len()); + for (i, span) in spans.into_iter().enumerate() { + debug_assert!( + (span.sep == ArgSep::End || i < nargs - 1) + || (span.sep != ArgSep::End || i == nargs - 1) + ); + match span.expr { + Some(expr) => exprs.push(expr), + None => unreachable!(), + } + } + exprs +} + +/// Operators that can appear within an expression. +/// +/// The main difference between this and `lexer::Token` is that, in here, we differentiate the +/// meaning of a minus sign and separate it in its two variants: the 2-operand `Minus` and the +/// 1-operand `Negate`. +/// +/// That said, this type also is the right place to abstract away operator-related logic to +/// implement the expression parsing algorithm, so it's not completely useless. +#[derive(Debug, Eq, PartialEq)] +enum ExprOp { + LeftParen, + + Add, + Subtract, + Multiply, + Divide, + Modulo, + Power, + Negate, + + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, + + And, + Not, + Or, + Xor, + + ShiftLeft, + ShiftRight, +} + +impl ExprOp { + /// Constructs a new operator based on a token, which must have a valid correspondence. + fn from(t: Token) -> Self { + match t { + Token::Equal => ExprOp::Equal, + Token::NotEqual => ExprOp::NotEqual, + Token::Less => ExprOp::Less, + Token::LessEqual => ExprOp::LessEqual, + Token::Greater => ExprOp::Greater, + Token::GreaterEqual => ExprOp::GreaterEqual, + Token::Plus => ExprOp::Add, + Token::Multiply => ExprOp::Multiply, + Token::Divide => ExprOp::Divide, + Token::Modulo => ExprOp::Modulo, + Token::Exponent => ExprOp::Power, + Token::And => ExprOp::And, + Token::Or => ExprOp::Or, + Token::Xor => ExprOp::Xor, + Token::ShiftLeft => ExprOp::ShiftLeft, + Token::ShiftRight => ExprOp::ShiftRight, + Token::Minus => panic!("Ambiguous token; cannot derive ExprOp"), + _ => panic!("Called on an non-operator"), + } + } + + /// Returns the priority of this operator. The specific number's meaning is only valid when + /// comparing it against other calls to this function. Higher number imply higher priority. + fn priority(&self) -> i8 { + match self { + ExprOp::LeftParen => 6, + ExprOp::Power => 6, + + ExprOp::Negate => 5, + ExprOp::Not => 5, + + ExprOp::Multiply => 4, + ExprOp::Divide => 4, + ExprOp::Modulo => 4, + + ExprOp::Add => 3, + ExprOp::Subtract => 3, + + ExprOp::ShiftLeft => 2, + ExprOp::ShiftRight => 2, + + ExprOp::Equal => 1, + ExprOp::NotEqual => 1, + ExprOp::Less => 1, + ExprOp::LessEqual => 1, + ExprOp::Greater => 1, + ExprOp::GreaterEqual => 1, + + ExprOp::And => 0, + ExprOp::Or => 0, + ExprOp::Xor => 0, + } + } +} + +/// Wrapper over an `ExprOp` to extend it with its position. +struct ExprOpSpan { + /// The wrapped expression operation. + op: ExprOp, + + /// The position where the operation appears in the input. + pos: LineCol, +} + +impl ExprOpSpan { + /// Creates a new span from its parts. + fn new(op: ExprOp, pos: LineCol) -> Self { + Self { op, pos } + } + + /// Pops operands from the `expr` stack, applies this operation, and pushes the result back. + fn apply(&self, exprs: &mut Vec) -> Result<()> { + fn apply1( + exprs: &mut Vec, + pos: LineCol, + f: fn(Box) -> Expr, + ) -> Result<()> { + if exprs.is_empty() { + return Err(Error::Bad(pos, "Not enough values to apply operator".to_owned())); + } + let expr = exprs.pop().unwrap(); + exprs.push(f(Box::from(UnaryOpSpan { expr, pos }))); + Ok(()) + } + + fn apply2( + exprs: &mut Vec, + pos: LineCol, + f: fn(Box) -> Expr, + ) -> Result<()> { + if exprs.len() < 2 { + return Err(Error::Bad(pos, "Not enough values to apply operator".to_owned())); + } + let rhs = exprs.pop().unwrap(); + let lhs = exprs.pop().unwrap(); + exprs.push(f(Box::from(BinaryOpSpan { lhs, rhs, pos }))); + Ok(()) + } + + match self.op { + ExprOp::Add => apply2(exprs, self.pos, Expr::Add), + ExprOp::Subtract => apply2(exprs, self.pos, Expr::Subtract), + ExprOp::Multiply => apply2(exprs, self.pos, Expr::Multiply), + ExprOp::Divide => apply2(exprs, self.pos, Expr::Divide), + ExprOp::Modulo => apply2(exprs, self.pos, Expr::Modulo), + ExprOp::Power => apply2(exprs, self.pos, Expr::Power), + + ExprOp::Equal => apply2(exprs, self.pos, Expr::Equal), + ExprOp::NotEqual => apply2(exprs, self.pos, Expr::NotEqual), + ExprOp::Less => apply2(exprs, self.pos, Expr::Less), + ExprOp::LessEqual => apply2(exprs, self.pos, Expr::LessEqual), + ExprOp::Greater => apply2(exprs, self.pos, Expr::Greater), + ExprOp::GreaterEqual => apply2(exprs, self.pos, Expr::GreaterEqual), + + ExprOp::And => apply2(exprs, self.pos, Expr::And), + ExprOp::Or => apply2(exprs, self.pos, Expr::Or), + ExprOp::Xor => apply2(exprs, self.pos, Expr::Xor), + + ExprOp::ShiftLeft => apply2(exprs, self.pos, Expr::ShiftLeft), + ExprOp::ShiftRight => apply2(exprs, self.pos, Expr::ShiftRight), + + ExprOp::Negate => apply1(exprs, self.pos, Expr::Negate), + ExprOp::Not => apply1(exprs, self.pos, Expr::Not), + + ExprOp::LeftParen => Ok(()), + } + } +} + +/// Iterator over the statements of the language. +pub struct Parser<'a> { + lexer: PeekableLexer<'a>, +} + +impl<'a> Parser<'a> { + /// Creates a new parser from the given readable. + fn from(input: &'a mut dyn io::Read) -> Self { + Self { lexer: Lexer::from(input).peekable() } + } + + /// Expects the peeked token to be `t` and consumes it. Otherwise, leaves the token in the + /// stream and fails with error `err`. + fn expect_and_consume>(&mut self, t: Token, err: E) -> Result { + let peeked = self.lexer.peek()?; + if peeked.token != t { + return Err(Error::Bad(peeked.pos, err.into())); + } + Ok(self.lexer.consume_peeked()) + } + + /// Expects the peeked token to be `t` and consumes it. Otherwise, leaves the token in the + /// stream and fails with error `err`, pointing at `pos` as the original location of the + /// problem. + fn expect_and_consume_with_pos>( + &mut self, + t: Token, + pos: LineCol, + err: E, + ) -> Result<()> { + let peeked = self.lexer.peek()?; + if peeked.token != t { + return Err(Error::Bad(pos, err.into())); + } + self.lexer.consume_peeked(); + Ok(()) + } + + /// Reads statements until the `delim` keyword is found. The delimiter is not consumed. + fn parse_until(&mut self, delim: Token) -> Result> { + let mut stmts = vec![]; + loop { + let peeked = self.lexer.peek()?; + if peeked.token == delim { + break; + } else if peeked.token == Token::Eol { + self.lexer.consume_peeked(); + continue; + } + match self.parse_one_safe()? { + Some(stmt) => stmts.push(stmt), + None => break, + } + } + Ok(stmts) + } + + /// Parses an assignment for the variable reference `vref` already read. + fn parse_assignment(&mut self, vref: VarRef, vref_pos: LineCol) -> Result { + let expr = self.parse_required_expr("Missing expression in assignment")?; + + let next = self.lexer.peek()?; + match &next.token { + Token::Eof | Token::Eol | Token::Else => (), + t => return Err(Error::Bad(next.pos, format!("Unexpected {} in assignment", t))), + } + Ok(Statement::Assignment(AssignmentSpan { vref, vref_pos, expr })) + } + + /// Parses an assignment to the array `varref` with `subscripts`, both of which have already + /// been read. + fn parse_array_assignment( + &mut self, + vref: VarRef, + vref_pos: LineCol, + subscripts: Vec, + ) -> Result { + let expr = self.parse_required_expr("Missing expression in array assignment")?; + + let next = self.lexer.peek()?; + match &next.token { + Token::Eof | Token::Eol | Token::Else => (), + t => return Err(Error::Bad(next.pos, format!("Unexpected {} in array assignment", t))), + } + Ok(Statement::ArrayAssignment(ArrayAssignmentSpan { vref, vref_pos, subscripts, expr })) + } + + /// Parses a builtin call (things of the form `INPUT a`). + fn parse_builtin_call( + &mut self, + vref: VarRef, + vref_pos: LineCol, + mut first: Option, + ) -> Result { + let mut name = vref_to_unannotated_string(vref, vref_pos)?; + name.make_ascii_uppercase(); + + let mut args = vec![]; + loop { + let expr = self.parse_expr(first.take())?; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof | Token::Eol | Token::Else => { + if expr.is_some() || !args.is_empty() { + args.push(ArgSpan { expr, sep: ArgSep::End, sep_pos: peeked.pos }); + } + break; + } + Token::Semicolon => { + let peeked = self.lexer.consume_peeked(); + args.push(ArgSpan { expr, sep: ArgSep::Short, sep_pos: peeked.pos }); + } + Token::Comma => { + let peeked = self.lexer.consume_peeked(); + args.push(ArgSpan { expr, sep: ArgSep::Long, sep_pos: peeked.pos }); + } + Token::As => { + let peeked = self.lexer.consume_peeked(); + args.push(ArgSpan { expr, sep: ArgSep::As, sep_pos: peeked.pos }); + } + _ => { + return Err(Error::Bad( + peeked.pos, + "Expected comma, semicolon, or end of statement".to_owned(), + )); + } + } + } + Ok(Statement::Call(CallSpan { vref: VarRef::new(name, None), vref_pos, args })) + } + + /// Starts processing either an array reference or a builtin call and disambiguates between the + /// two. + fn parse_array_or_builtin_call( + &mut self, + vref: VarRef, + vref_pos: LineCol, + ) -> Result { + match self.lexer.peek()?.token { + Token::LeftParen => { + let left_paren = self.lexer.consume_peeked(); + let spans = self.parse_comma_separated_exprs()?; + let mut exprs = spans.into_iter().map(|span| span.expr.unwrap()).collect(); + match self.lexer.peek()?.token { + Token::Equal => { + self.lexer.consume_peeked(); + self.parse_array_assignment(vref, vref_pos, exprs) + } + _ => { + if exprs.len() != 1 { + return Err(Error::Bad( + left_paren.pos, + "Expected expression".to_owned(), + )); + } + self.parse_builtin_call(vref, vref_pos, Some(exprs.remove(0))) + } + } + } + _ => self.parse_builtin_call(vref, vref_pos, None), + } + } + + /// Parses the type name of an `AS` type definition. + /// + /// The `AS` token has already been consumed, so all this does is read a literal type name and + /// convert it to the corresponding expression type. + fn parse_as_type(&mut self) -> Result<(ExprType, LineCol)> { + let token_span = self.lexer.read()?; + match token_span.token { + Token::BooleanName => Ok((ExprType::Boolean, token_span.pos)), + Token::DoubleName => Ok((ExprType::Double, token_span.pos)), + Token::IntegerName => Ok((ExprType::Integer, token_span.pos)), + Token::TextName => Ok((ExprType::Text, token_span.pos)), + t => Err(Error::Bad( + token_span.pos, + format!("Invalid type name {} in AS type definition", t), + )), + } + } + + /// Parses a `DATA` statement. + fn parse_data(&mut self) -> Result { + let mut values = vec![]; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof | Token::Eol | Token::Else => { + values.push(None); + break; + } + _ => (), + } + + let token_span = self.lexer.read()?; + match token_span.token { + Token::Boolean(b) => { + values.push(Some(Expr::Boolean(BooleanSpan { value: b, pos: token_span.pos }))) + } + Token::Double(d) => { + values.push(Some(Expr::Double(DoubleSpan { value: d, pos: token_span.pos }))) + } + Token::Integer(i) => { + values.push(Some(Expr::Integer(IntegerSpan { value: i, pos: token_span.pos }))) + } + Token::Text(t) => { + values.push(Some(Expr::Text(TextSpan { value: t, pos: token_span.pos }))) + } + + Token::Minus => { + let token_span2 = self.lexer.read()?; + match token_span2.token { + Token::Double(d) => values.push(Some(Expr::Double(DoubleSpan { + value: -d, + pos: token_span.pos, + }))), + Token::Integer(i) => values.push(Some(Expr::Integer(IntegerSpan { + value: -i, + pos: token_span.pos, + }))), + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected number after -".to_owned(), + )); + } + } + } + + Token::Eof | Token::Eol | Token::Else => { + panic!("Should not be consumed here; handled above") + } + + Token::Comma => { + values.push(None); + continue; + } + + t => { + return Err(Error::Bad( + token_span.pos, + format!("Unexpected {} in DATA statement", t), + )); + } + } + + let peeked = self.lexer.peek()?; + match &peeked.token { + Token::Eof | Token::Eol | Token::Else => { + break; + } + + Token::Comma => { + self.lexer.consume_peeked(); + } + + t => { + return Err(Error::Bad( + peeked.pos, + format!("Expected comma after datum but found {}", t), + )); + } + } + } + Ok(Statement::Data(DataSpan { values })) + } + + /// Parses the `AS typename` clause of a `DIM` statement. The caller has already consumed the + /// `AS` token. + fn parse_dim_as(&mut self) -> Result<(ExprType, LineCol)> { + let peeked = self.lexer.peek()?; + let (vtype, vtype_pos) = match peeked.token { + Token::Eof | Token::Eol => (ExprType::Integer, peeked.pos), + Token::As => { + self.lexer.consume_peeked(); + self.parse_as_type()? + } + _ => return Err(Error::Bad(peeked.pos, "Expected AS or end of statement".to_owned())), + }; + + let next = self.lexer.peek()?; + match &next.token { + Token::Eof | Token::Eol => (), + t => return Err(Error::Bad(next.pos, format!("Unexpected {} in DIM statement", t))), + } + + Ok((vtype, vtype_pos)) + } + + /// Parses a `DIM` statement. + fn parse_dim(&mut self) -> Result { + let peeked = self.lexer.peek()?; + let mut shared = false; + if peeked.token == Token::Shared { + self.lexer.consume_peeked(); + shared = true; + } + + let token_span = self.lexer.read()?; + let vref = match token_span.token { + Token::Symbol(vref) => vref, + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected variable name after DIM".to_owned(), + )); + } + }; + let name = vref_to_unannotated_string(vref, token_span.pos)?; + let name_pos = token_span.pos; + + match self.lexer.peek()?.token { + Token::LeftParen => { + let peeked = self.lexer.consume_peeked(); + let dimensions = self.parse_comma_separated_exprs()?; + if dimensions.is_empty() { + return Err(Error::Bad( + peeked.pos, + "Arrays require at least one dimension".to_owned(), + )); + } + let (subtype, subtype_pos) = self.parse_dim_as()?; + Ok(Statement::DimArray(DimArraySpan { + name, + name_pos, + shared, + dimensions: argspans_to_exprs(dimensions), + subtype, + subtype_pos, + })) + } + _ => { + let (vtype, vtype_pos) = self.parse_dim_as()?; + Ok(Statement::Dim(DimSpan { name, name_pos, shared, vtype, vtype_pos })) + } + } + } + + /// Parses the `UNTIL` or `WHILE` clause of a `DO` loop. + /// + /// `part` is a string indicating where the clause is expected (either after `DO` or after + /// `LOOP`). + /// + /// Returns the guard expression and a boolean indicating if this is an `UNTIL` clause. + fn parse_do_guard(&mut self, part: &str) -> Result> { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Until => { + self.lexer.consume_peeked(); + let expr = self.parse_required_expr("No expression in UNTIL clause")?; + Ok(Some((expr, true))) + } + Token::While => { + self.lexer.consume_peeked(); + let expr = self.parse_required_expr("No expression in WHILE clause")?; + Ok(Some((expr, false))) + } + Token::Eof | Token::Eol => Ok(None), + _ => { + let token_span = self.lexer.consume_peeked(); + Err(Error::Bad( + token_span.pos, + format!("Expecting newline, UNTIL or WHILE after {}", part), + )) + } + } + } + + /// Parses a `DO` statement. + fn parse_do(&mut self, do_pos: LineCol) -> Result { + let pre_guard = self.parse_do_guard("DO")?; + self.expect_and_consume(Token::Eol, "Expecting newline after DO")?; + + let stmts = self.parse_until(Token::Loop)?; + self.expect_and_consume_with_pos(Token::Loop, do_pos, "DO without LOOP")?; + + let post_guard = self.parse_do_guard("LOOP")?; + + let guard = match (pre_guard, post_guard) { + (None, None) => DoGuard::Infinite, + (Some((guard, true)), None) => DoGuard::PreUntil(guard), + (Some((guard, false)), None) => DoGuard::PreWhile(guard), + (None, Some((guard, true))) => DoGuard::PostUntil(guard), + (None, Some((guard, false))) => DoGuard::PostWhile(guard), + (Some(_), Some(_)) => { + return Err(Error::Bad( + do_pos, + "DO loop cannot have pre and post guards at the same time".to_owned(), + )); + } + }; + + Ok(Statement::Do(DoSpan { guard, body: stmts })) + } + + /// Advances until the next statement after failing to parse a `DO` statement. + fn reset_do(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Loop => { + self.lexer.consume_peeked(); + loop { + match self.lexer.peek()?.token { + Token::Eof | Token::Eol => break, + _ => { + self.lexer.consume_peeked(); + } + } + } + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses a potential `END` statement but, if this corresponds to a statement terminator such + /// as `END IF`, returns the token that followed `END`. + fn maybe_parse_end(&mut self) -> Result> { + match self.lexer.peek()?.token { + Token::Function => Ok(Err(Token::Function)), + Token::If => Ok(Err(Token::If)), + Token::Select => Ok(Err(Token::Select)), + Token::Sub => Ok(Err(Token::Sub)), + _ => { + let code = self.parse_expr(None)?; + Ok(Ok(Statement::End(EndSpan { code }))) + } + } + } + + /// Parses an `END` statement. + fn parse_end(&mut self, pos: LineCol) -> Result { + match self.maybe_parse_end()? { + Ok(stmt) => Ok(stmt), + Err(token) => Err(Error::Bad(pos, format!("END {} without {}", token, token))), + } + } + + /// Parses an `EXIT` statement. + fn parse_exit(&mut self, pos: LineCol) -> Result { + let peeked = self.lexer.peek()?; + let stmt = match peeked.token { + Token::Do => Statement::ExitDo(ExitSpan { pos }), + Token::For => Statement::ExitFor(ExitSpan { pos }), + Token::Function => Statement::ExitFunction(ExitSpan { pos }), + Token::Sub => Statement::ExitSub(ExitSpan { pos }), + _ => { + return Err(Error::Bad( + peeked.pos, + "Expecting DO, FOR, FUNCTION or SUB after EXIT".to_owned(), + )); + } + }; + self.lexer.consume_peeked(); + Ok(stmt) + } + + /// Parses a variable list of comma-separated expressions. The caller must have consumed the + /// open parenthesis and we stop processing when we encounter the terminating parenthesis (and + /// consume it). We expect at least one expression. + fn parse_comma_separated_exprs(&mut self) -> Result> { + let mut spans = vec![]; + + // The first expression is optional to support calls to functions without arguments. + let mut is_first = true; + let mut prev_expr = self.parse_expr(None)?; + + loop { + let peeked = self.lexer.peek()?; + let pos = peeked.pos; + match &peeked.token { + Token::RightParen => { + self.lexer.consume_peeked(); + + if let Some(expr) = prev_expr.take() { + spans.push(ArgSpan { expr: Some(expr), sep: ArgSep::End, sep_pos: pos }); + } else { + if !is_first { + return Err(Error::Bad(pos, "Missing expression".to_owned())); + } + } + + break; + } + Token::Comma => { + self.lexer.consume_peeked(); + + if let Some(expr) = prev_expr.take() { + // The first expression is optional to support calls to functions without + // arguments. + spans.push(ArgSpan { expr: Some(expr), sep: ArgSep::Long, sep_pos: pos }); + } else { + return Err(Error::Bad(pos, "Missing expression".to_owned())); + } + + prev_expr = self.parse_expr(None)?; + } + t => return Err(Error::Bad(pos, format!("Unexpected {}", t))), + } + + is_first = false; + } + + Ok(spans) + } + + /// Parses an expression. + /// + /// Returns `None` if no expression was found. This is necessary to treat the case of empty + /// arguments to statements, as is the case in `PRINT a , , b`. + /// + /// If the caller has already processed a parenthesized term of an expression like + /// `(first) + second`, then that term must be provided in `first`. + /// + /// This is an implementation of the Shunting Yard Algorithm by Edgar Dijkstra. + fn parse_expr(&mut self, first: Option) -> Result> { + let mut exprs: Vec = vec![]; + let mut op_spans: Vec = vec![]; + + let mut need_operand = true; // Also tracks whether an upcoming minus is unary. + if let Some(e) = first { + exprs.push(e); + need_operand = false; + } + + loop { + let mut handle_operand = |e, pos| { + if !need_operand { + return Err(Error::Bad(pos, "Unexpected value in expression".to_owned())); + } + need_operand = false; + exprs.push(e); + Ok(()) + }; + + // Stop processing if we encounter an expression separator, but don't consume it because + // the caller needs to have access to it. + match self.lexer.peek()?.token { + Token::Eof + | Token::Eol + | Token::As + | Token::Comma + | Token::Else + | Token::Semicolon + | Token::Then + | Token::To + | Token::Step => break, + Token::RightParen => { + if !op_spans.iter().any(|eos| eos.op == ExprOp::LeftParen) { + // We encountered an unbalanced parenthesis but we don't know if this is + // because we were called from within an argument list (in which case the + // caller consumed the opening parenthesis and is expecting to consume the + // closing parenthesis) or because we really found an invalid expression. + // Only the caller can know, so avoid consuming the token and exit. + break; + } + } + _ => (), + }; + + let ts = self.lexer.consume_peeked(); + match ts.token { + Token::Boolean(value) => { + handle_operand(Expr::Boolean(BooleanSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Double(value) => { + handle_operand(Expr::Double(DoubleSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Integer(value) => { + handle_operand(Expr::Integer(IntegerSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Text(value) => { + handle_operand(Expr::Text(TextSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Symbol(vref) => { + handle_operand(Expr::Symbol(SymbolSpan { vref, pos: ts.pos }), ts.pos)? + } + + Token::LeftParen => { + // If the last operand we encountered was a symbol, collapse it and the left + // parenthesis into the beginning of a function call. + match exprs.pop() { + Some(Expr::Symbol(span)) => { + if !need_operand { + exprs.push(Expr::Call(CallSpan { + vref: span.vref, + vref_pos: span.pos, + args: self.parse_comma_separated_exprs()?, + })); + need_operand = false; + } else { + // We popped out the last expression to see if it this left + // parenthesis started a function call... but it did not (it is a + // symbol following a parenthesis) so put both the expression and + // the token back. + op_spans.push(ExprOpSpan::new(ExprOp::LeftParen, ts.pos)); + exprs.push(Expr::Symbol(span)); + need_operand = true; + } + } + e => { + if let Some(e) = e { + // We popped out the last expression to see if this left + // parenthesis started a function call... but if it didn't, we have + // to put the expression back. + exprs.push(e); + } + if !need_operand { + return Err(Error::Bad( + ts.pos, + format!("Unexpected {} in expression", ts.token), + )); + } + op_spans.push(ExprOpSpan::new(ExprOp::LeftParen, ts.pos)); + need_operand = true; + } + }; + } + Token::RightParen => { + let mut found = false; + while let Some(eos) = op_spans.pop() { + eos.apply(&mut exprs)?; + if eos.op == ExprOp::LeftParen { + found = true; + break; + } + } + assert!(found, "Unbalanced parenthesis should have been handled above"); + need_operand = false; + } + + Token::Not => { + op_spans.push(ExprOpSpan::new(ExprOp::Not, ts.pos)); + need_operand = true; + } + Token::Minus => { + let op; + if need_operand { + op = ExprOp::Negate; + } else { + op = ExprOp::Subtract; + while let Some(eos2) = op_spans.last() { + if eos2.op == ExprOp::LeftParen || eos2.op.priority() < op.priority() { + break; + } + let eos2 = op_spans.pop().unwrap(); + eos2.apply(&mut exprs)?; + } + } + op_spans.push(ExprOpSpan::new(op, ts.pos)); + need_operand = true; + } + + Token::Equal + | Token::NotEqual + | Token::Less + | Token::LessEqual + | Token::Greater + | Token::GreaterEqual + | Token::Plus + | Token::Multiply + | Token::Divide + | Token::Modulo + | Token::Exponent + | Token::And + | Token::Or + | Token::Xor + | Token::ShiftLeft + | Token::ShiftRight => { + let op = ExprOp::from(ts.token); + while let Some(eos2) = op_spans.last() { + if eos2.op == ExprOp::LeftParen || eos2.op.priority() < op.priority() { + break; + } + let eos2 = op_spans.pop().unwrap(); + eos2.apply(&mut exprs)?; + } + op_spans.push(ExprOpSpan::new(op, ts.pos)); + need_operand = true; + } + + Token::Bad(e) => return Err(Error::Bad(ts.pos, e)), + + Token::Eof + | Token::Eol + | Token::As + | Token::Comma + | Token::Else + | Token::Semicolon + | Token::Then + | Token::To + | Token::Step => { + panic!("Field separators handled above") + } + + Token::BooleanName + | Token::Case + | Token::Data + | Token::Do + | Token::Dim + | Token::DoubleName + | Token::Elseif + | Token::End + | Token::Error + | Token::Exit + | Token::For + | Token::Function + | Token::Gosub + | Token::Goto + | Token::If + | Token::Is + | Token::IntegerName + | Token::Label(_) + | Token::Loop + | Token::Next + | Token::On + | Token::Resume + | Token::Return + | Token::Select + | Token::Shared + | Token::Sub + | Token::TextName + | Token::Until + | Token::Wend + | Token::While => { + return Err(Error::Bad(ts.pos, "Unexpected keyword in expression".to_owned())); + } + }; + } + + while let Some(eos) = op_spans.pop() { + match eos.op { + ExprOp::LeftParen => { + return Err(Error::Bad(eos.pos, "Unbalanced parenthesis".to_owned())); + } + _ => eos.apply(&mut exprs)?, + } + } + + if let Some(expr) = exprs.pop() { Ok(Some(expr)) } else { Ok(None) } + } + + /// Wrapper over `parse_expr` that requires an expression to be present and returns an error + /// with `msg` otherwise. + fn parse_required_expr(&mut self, msg: &'static str) -> Result { + let next_pos = self.lexer.peek()?.pos; + match self.parse_expr(None)? { + Some(expr) => Ok(expr), + None => Err(Error::Bad(next_pos, msg.to_owned())), + } + } + + /// Parses a `GOSUB` statement. + fn parse_gosub(&mut self) -> Result { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Integer(i) => { + let target = format!("{}", i); + Ok(Statement::Gosub(GotoSpan { target, target_pos: token_span.pos })) + } + Token::Label(target) => { + Ok(Statement::Gosub(GotoSpan { target, target_pos: token_span.pos })) + } + _ => Err(Error::Bad(token_span.pos, "Expected label name after GOSUB".to_owned())), + } + } + + /// Parses a `GOTO` statement. + fn parse_goto(&mut self) -> Result { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Integer(i) => { + let target = format!("{}", i); + Ok(Statement::Goto(GotoSpan { target, target_pos: token_span.pos })) + } + Token::Label(target) => { + Ok(Statement::Goto(GotoSpan { target, target_pos: token_span.pos })) + } + _ => Err(Error::Bad(token_span.pos, "Expected label name after GOTO".to_owned())), + } + } + + /// Parses the branches of a uniline `IF` statement. + fn parse_if_uniline(&mut self, branches: &mut Vec) -> Result<()> { + debug_assert!(!branches.is_empty(), "Caller must populate the guard of the first branch"); + + let mut has_else = false; + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Else => has_else = true, + _ => { + let stmt = self + .parse_uniline()? + .expect("The caller already checked for a non-empty token"); + branches[0].body.push(stmt); + } + } + + let peeked = self.lexer.peek()?; + has_else |= peeked.token == Token::Else; + + if has_else { + let else_span = self.lexer.consume_peeked(); + let expr = Expr::Boolean(BooleanSpan { value: true, pos: else_span.pos }); + branches.push(IfBranchSpan { guard: expr, body: vec![] }); + if let Some(stmt) = self.parse_uniline()? { + branches[1].body.push(stmt); + } + } + + Ok(()) + } + + /// Parses the branches of a multiline `IF` statement. + fn parse_if_multiline( + &mut self, + if_pos: LineCol, + branches: &mut Vec, + ) -> Result<()> { + debug_assert!(!branches.is_empty(), "Caller must populate the guard of the first branch"); + + let mut i = 0; + let mut last = false; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol => { + self.lexer.consume_peeked(); + } + + Token::Elseif => { + if last { + return Err(Error::Bad( + peeked.pos, + "Unexpected ELSEIF after ELSE".to_owned(), + )); + } + + self.lexer.consume_peeked(); + let expr = self.parse_required_expr("No expression in ELSEIF statement")?; + self.expect_and_consume(Token::Then, "No THEN in ELSEIF statement")?; + self.expect_and_consume(Token::Eol, "Expecting newline after THEN")?; + branches.push(IfBranchSpan { guard: expr, body: vec![] }); + i += 1; + } + + Token::Else => { + if last { + return Err(Error::Bad(peeked.pos, "Duplicate ELSE after ELSE".to_owned())); + } + + let else_span = self.lexer.consume_peeked(); + self.expect_and_consume(Token::Eol, "Expecting newline after ELSE")?; + + let expr = Expr::Boolean(BooleanSpan { value: true, pos: else_span.pos }); + branches.push(IfBranchSpan { guard: expr, body: vec![] }); + i += 1; + + last = true; + } + + Token::End => { + let token_span = self.lexer.consume_peeked(); + match self.maybe_parse_end()? { + Ok(stmt) => { + branches[i].body.push(stmt); + } + Err(Token::If) => { + break; + } + Err(token) => { + return Err(Error::Bad( + token_span.pos, + format!("END {} without {}", token, token), + )); + } + } + } + + _ => match self.parse_one_safe()? { + Some(stmt) => { + branches[i].body.push(stmt); + } + None => { + break; + } + }, + } + } + + self.expect_and_consume_with_pos(Token::If, if_pos, "IF without END IF") + } + + /// Parses an `IF` statement. + fn parse_if(&mut self, if_pos: LineCol) -> Result { + let expr = self.parse_required_expr("No expression in IF statement")?; + self.expect_and_consume(Token::Then, "No THEN in IF statement")?; + + let mut branches = vec![IfBranchSpan { guard: expr, body: vec![] }]; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol | Token::Eof => self.parse_if_multiline(if_pos, &mut branches)?, + _ => self.parse_if_uniline(&mut branches)?, + } + + Ok(Statement::If(IfSpan { branches })) + } + + /// Advances until the next statement after failing to parse an `IF` statement. + fn reset_if(&mut self, if_pos: LineCol) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::End => { + self.lexer.consume_peeked(); + self.expect_and_consume_with_pos(Token::If, if_pos, "IF without END IF")?; + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Extracts the optional `STEP` part of a `FOR` statement, with a default of 1. + /// + /// Returns the step as an expression, an `Ordering` value representing how the step value + /// compares to zero, and whether the step is a double or not. + fn parse_step(&mut self) -> Result<(Expr, Ordering, bool)> { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Step => self.lexer.consume_peeked(), + _ => { + // The position we return here for the step isn't truly the right value, but given + // that we know the hardcoded step of 1 is valid, the caller will not error out and + // will not print the slightly invalid position. + return Ok(( + Expr::Integer(IntegerSpan { value: 1, pos: peeked.pos }), + Ordering::Greater, + false, + )); + } + }; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Double(d) => { + let peeked = self.lexer.consume_peeked(); + let sign = if d == 0.0 { Ordering::Equal } else { Ordering::Greater }; + Ok((Expr::Double(DoubleSpan { value: d, pos: peeked.pos }), sign, true)) + } + Token::Integer(i) => { + let peeked = self.lexer.consume_peeked(); + Ok((Expr::Integer(IntegerSpan { value: i, pos: peeked.pos }), i.cmp(&0), false)) + } + Token::Minus => { + self.lexer.consume_peeked(); + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Double(d) => { + let peeked = self.lexer.consume_peeked(); + let sign = if d == 0.0 { Ordering::Equal } else { Ordering::Less }; + Ok((Expr::Double(DoubleSpan { value: -d, pos: peeked.pos }), sign, true)) + } + Token::Integer(i) => { + let peeked = self.lexer.consume_peeked(); + Ok(( + Expr::Integer(IntegerSpan { value: -i, pos: peeked.pos }), + (-i).cmp(&0), + false, + )) + } + _ => Err(Error::Bad(peeked.pos, "STEP needs a literal number".to_owned())), + } + } + _ => Err(Error::Bad(peeked.pos, "STEP needs a literal number".to_owned())), + } + } + + /// Parses a `FOR` statement. + fn parse_for(&mut self, for_pos: LineCol) -> Result { + let token_span = self.lexer.read()?; + let iterator = match token_span.token { + Token::Symbol(iterator) => match iterator.ref_type { + None | Some(ExprType::Double) | Some(ExprType::Integer) => iterator, + _ => { + return Err(Error::Bad( + token_span.pos, + "Iterator name in FOR statement must be a numeric reference".to_owned(), + )); + } + }, + _ => { + return Err(Error::Bad( + token_span.pos, + "No iterator name in FOR statement".to_owned(), + )); + } + }; + let iterator_pos = token_span.pos; + + self.expect_and_consume(Token::Equal, "No equal sign in FOR statement")?; + let start = self.parse_required_expr("No start expression in FOR statement")?; + + let to_span = self.expect_and_consume(Token::To, "No TO in FOR statement")?; + let end = self.parse_required_expr("No end expression in FOR statement")?; + + let (step, step_sign, iter_double) = self.parse_step()?; + let end_condition = match step_sign { + Ordering::Greater => Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: Expr::Symbol(SymbolSpan { vref: iterator.clone(), pos: iterator_pos }), + rhs: end, + pos: to_span.pos, + })), + Ordering::Less => Expr::GreaterEqual(Box::from(BinaryOpSpan { + lhs: Expr::Symbol(SymbolSpan { vref: iterator.clone(), pos: iterator_pos }), + rhs: end, + pos: to_span.pos, + })), + Ordering::Equal => { + return Err(Error::Bad( + step.start_pos(), + "Infinite FOR loop; STEP cannot be 0".to_owned(), + )); + } + }; + + let next_value = Expr::Add(Box::from(BinaryOpSpan { + lhs: Expr::Symbol(SymbolSpan { vref: iterator.clone(), pos: iterator_pos }), + rhs: step, + pos: to_span.pos, + })); + + self.expect_and_consume(Token::Eol, "Expecting newline after FOR")?; + + let stmts = self.parse_until(Token::Next)?; + self.expect_and_consume_with_pos(Token::Next, for_pos, "FOR without NEXT")?; + + Ok(Statement::For(ForSpan { + iter: iterator, + iter_pos: iterator_pos, + iter_double, + start, + end: end_condition, + next: next_value, + body: stmts, + })) + } + + /// Advances until the next statement after failing to parse a `FOR` statement. + fn reset_for(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Next => { + self.lexer.consume_peeked(); + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses the optional parameter list that may appear after a `FUNCTION` or `SUB` definition, + /// including the opening and closing parenthesis. + fn parse_callable_args(&mut self) -> Result> { + let mut params = vec![]; + let peeked = self.lexer.peek()?; + if peeked.token == Token::LeftParen { + self.lexer.consume_peeked(); + + loop { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Symbol(param) => { + let peeked = self.lexer.peek()?; + if peeked.token == Token::As { + self.lexer.consume_peeked(); + + let name = vref_to_unannotated_string(param, token_span.pos)?; + let (vtype, _pos) = self.parse_as_type()?; + params.push(VarRef::new(name, Some(vtype))); + } else { + params.push(param); + } + } + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected a parameter name".to_owned(), + )); + } + } + + let token_span = self.lexer.read()?; + match token_span.token { + Token::Comma => (), + Token::RightParen => break, + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected comma, AS, or end of parameters list".to_owned(), + )); + } + } + } + } + Ok(params) + } + + /// Parses the body of a callable and returns the collection of statements and the position + /// of the end of the body. + fn parse_callable_body( + &mut self, + start_pos: LineCol, + exp_token: Token, + ) -> Result<(Vec, LineCol)> { + debug_assert!(matches!(exp_token, Token::Function | Token::Sub)); + + let mut body = vec![]; + let end_pos; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof => { + end_pos = peeked.pos; + break; + } + + Token::Eol => { + self.lexer.consume_peeked(); + } + + Token::Function | Token::Sub => { + return Err(Error::Bad( + peeked.pos, + "Cannot nest FUNCTION or SUB definitions".to_owned(), + )); + } + + Token::End => { + let end_span = self.lexer.consume_peeked(); + match self.maybe_parse_end()? { + Ok(stmt) => { + body.push(stmt); + } + Err(token) if token == exp_token => { + end_pos = end_span.pos; + break; + } + Err(token) => { + return Err(Error::Bad( + end_span.pos, + format!("END {} without {}", token, token), + )); + } + } + } + + _ => match self.parse_one_safe()? { + Some(stmt) => body.push(stmt), + None => { + return Err(Error::Bad( + start_pos, + format!("{} without END {}", exp_token, exp_token), + )); + } + }, + } + } + + self.expect_and_consume_with_pos( + exp_token.clone(), + start_pos, + format!("{} without END {}", exp_token, exp_token), + )?; + + Ok((body, end_pos)) + } + + /// Parses a `FUNCTION` definition. + fn parse_function(&mut self, function_pos: LineCol) -> Result { + let token_span = self.lexer.read()?; + let name = match token_span.token { + Token::Symbol(name) => { + if name.ref_type.is_none() { + VarRef::new(name.name, Some(ExprType::Integer)) + } else { + name + } + } + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected a function name after FUNCTION".to_owned(), + )); + } + }; + let name_pos = token_span.pos; + + let params = self.parse_callable_args()?; + self.expect_and_consume(Token::Eol, "Expected newline after FUNCTION name")?; + + let (body, end_pos) = self.parse_callable_body(function_pos, Token::Function)?; + + Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) + } + + /// Parses a `SUB` definition. + fn parse_sub(&mut self, sub_pos: LineCol) -> Result { + let token_span = self.lexer.read()?; + let name = match token_span.token { + Token::Symbol(name) => { + if name.ref_type.is_some() { + return Err(Error::Bad( + token_span.pos, + "SUBs cannot return a value so type annotations are not allowed".to_owned(), + )); + } + name + } + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected a function name after SUB".to_owned(), + )); + } + }; + let name_pos = token_span.pos; + + let params = self.parse_callable_args()?; + self.expect_and_consume(Token::Eol, "Expected newline after SUB name")?; + + let (body, end_pos) = self.parse_callable_body(sub_pos, Token::Sub)?; + + Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) + } + + /// Advances until the next statement after failing to parse a `FUNCTION` or `SUB` definition. + fn reset_callable(&mut self, exp_token: Token) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::End => { + self.lexer.consume_peeked(); + + let token_span = self.lexer.read()?; + if token_span.token == exp_token { + break; + } + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses an `ON ERROR` statement. Only `ON` has been consumed so far. + fn parse_on(&mut self) -> Result { + self.expect_and_consume(Token::Error, "Expected ERROR after ON")?; + + let token_span = self.lexer.read()?; + match token_span.token { + Token::Goto => { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Integer(0) => Ok(Statement::OnError(OnErrorSpan::Reset)), + Token::Integer(i) => Ok(Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target: format!("{}", i), + target_pos: token_span.pos, + }))), + Token::Label(target) => Ok(Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target, + target_pos: token_span.pos, + }))), + _ => Err(Error::Bad( + token_span.pos, + "Expected label name or 0 after ON ERROR GOTO".to_owned(), + )), + } + } + Token::Resume => { + self.expect_and_consume(Token::Next, "Expected NEXT after ON ERROR RESUME")?; + Ok(Statement::OnError(OnErrorSpan::ResumeNext)) + } + _ => { + Err(Error::Bad(token_span.pos, "Expected GOTO or RESUME after ON ERROR".to_owned())) + } + } + } + + /// Parses the guards after a `CASE` keyword. + fn parse_case_guards(&mut self) -> Result> { + let mut guards = vec![]; + + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Else => { + let token_span = self.lexer.consume_peeked(); + + if !guards.is_empty() { + return Err(Error::Bad( + token_span.pos, + "CASE ELSE must be on its own".to_owned(), + )); + } + + let peeked = self.lexer.peek()?; + if peeked.token != Token::Eol && peeked.token != Token::Eof { + return Err(Error::Bad( + peeked.pos, + "Expected newline after CASE ELSE".to_owned(), + )); + } + + break; + } + + Token::Is => { + self.lexer.consume_peeked(); + + let token_span = self.lexer.read()?; + let rel_op = match token_span.token { + Token::Equal => CaseRelOp::Equal, + Token::NotEqual => CaseRelOp::NotEqual, + Token::Less => CaseRelOp::Less, + Token::LessEqual => CaseRelOp::LessEqual, + Token::Greater => CaseRelOp::Greater, + Token::GreaterEqual => CaseRelOp::GreaterEqual, + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected relational operator".to_owned(), + )); + } + }; + + let expr = + self.parse_required_expr("Missing expression after relational operator")?; + guards.push(CaseGuardSpan::Is(rel_op, expr)); + } + + _ => { + let from_expr = self.parse_required_expr("Missing expression in CASE guard")?; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol | Token::Comma => { + guards.push(CaseGuardSpan::Is(CaseRelOp::Equal, from_expr)); + } + Token::To => { + self.lexer.consume_peeked(); + let to_expr = self + .parse_required_expr("Missing expression after TO in CASE guard")?; + guards.push(CaseGuardSpan::To(from_expr, to_expr)); + } + _ => { + return Err(Error::Bad( + peeked.pos, + "Expected comma, newline, or TO after expression".to_owned(), + )); + } + } + } + } + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol => { + break; + } + Token::Comma => { + self.lexer.consume_peeked(); + } + _ => { + return Err(Error::Bad( + peeked.pos, + "Expected comma, newline, or TO after expression".to_owned(), + )); + } + } + } + + Ok(guards) + } + + /// Parses a `SELECT` statement. + fn parse_select(&mut self, select_pos: LineCol) -> Result { + self.expect_and_consume(Token::Case, "Expecting CASE after SELECT")?; + + let expr = self.parse_required_expr("No expression in SELECT CASE statement")?; + self.expect_and_consume(Token::Eol, "Expecting newline after SELECT CASE")?; + + let mut cases = vec![]; + + let mut i = 0; + let mut last = false; + let end_pos; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof => { + end_pos = peeked.pos; + break; + } + + Token::Eol => { + self.lexer.consume_peeked(); + } + + Token::Case => { + let peeked = self.lexer.consume_peeked(); + let guards = self.parse_case_guards()?; + self.expect_and_consume(Token::Eol, "Expecting newline after CASE")?; + + let is_last = guards.is_empty(); + if last { + if is_last { + return Err(Error::Bad( + peeked.pos, + "CASE ELSE must be unique".to_owned(), + )); + } else { + return Err(Error::Bad(peeked.pos, "CASE ELSE is not last".to_owned())); + } + } + last |= is_last; + + cases.push(CaseSpan { guards, body: vec![] }); + if cases.len() > 1 { + i += 1; + } + } + + Token::End => { + let end_span = self.lexer.consume_peeked(); + match self.maybe_parse_end()? { + Ok(stmt) => { + if cases.is_empty() { + return Err(Error::Bad( + end_span.pos, + "Expected CASE after SELECT CASE before any statement" + .to_owned(), + )); + } + + cases[i].body.push(stmt); + } + Err(Token::Select) => { + end_pos = end_span.pos; + break; + } + Err(token) => { + if cases.is_empty() { + return Err(Error::Bad( + end_span.pos, + "Expected CASE after SELECT CASE before any statement" + .to_owned(), + )); + } else { + return Err(Error::Bad( + end_span.pos, + format!("END {} without {}", token, token), + )); + } + } + } + } + + _ => { + if cases.is_empty() { + return Err(Error::Bad( + peeked.pos, + "Expected CASE after SELECT CASE before any statement".to_owned(), + )); + } + + if let Some(stmt) = self.parse_one_safe()? { + cases[i].body.push(stmt); + } + } + } + } + + self.expect_and_consume_with_pos(Token::Select, select_pos, "SELECT without END SELECT")?; + + Ok(Statement::Select(SelectSpan { expr, cases, end_pos })) + } + + /// Advances until the next statement after failing to parse a `SELECT` statement. + fn reset_select(&mut self, select_pos: LineCol) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::End => { + self.lexer.consume_peeked(); + self.expect_and_consume_with_pos( + Token::Select, + select_pos, + "SELECT without END SELECT", + )?; + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses a `WHILE` statement. + fn parse_while(&mut self, while_pos: LineCol) -> Result { + let expr = self.parse_required_expr("No expression in WHILE statement")?; + self.expect_and_consume(Token::Eol, "Expecting newline after WHILE")?; + + let stmts = self.parse_until(Token::Wend)?; + self.expect_and_consume_with_pos(Token::Wend, while_pos, "WHILE without WEND")?; + + Ok(Statement::While(WhileSpan { expr, body: stmts })) + } + + /// Advances until the next statement after failing to parse a `WHILE` statement. + fn reset_while(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Wend => { + self.lexer.consume_peeked(); + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Extracts the next available uniline statement from the input stream, or `None` if none is + /// available. + /// + /// The statement must be specifiable in a single line as part of a uniline `IF` statement, and + /// we currently expect this to only be used while parsing an `IF`. + /// + /// On success, the stream is left in a position where the next statement can be extracted. + /// On failure, the caller must advance the stream to the next statement by calling `reset`. + fn parse_uniline(&mut self) -> Result> { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Data => Ok(Some(self.parse_data()?)), + Token::End => Ok(Some(self.parse_end(token_span.pos)?)), + Token::Eof | Token::Eol => Ok(None), + Token::Exit => Ok(Some(self.parse_exit(token_span.pos)?)), + Token::Gosub => Ok(Some(self.parse_gosub()?)), + Token::Goto => Ok(Some(self.parse_goto()?)), + Token::On => Ok(Some(self.parse_on()?)), + Token::Return => Ok(Some(Statement::Return(ReturnSpan { pos: token_span.pos }))), + Token::Symbol(vref) => { + let peeked = self.lexer.peek()?; + if peeked.token == Token::Equal { + self.lexer.consume_peeked(); + Ok(Some(self.parse_assignment(vref, token_span.pos)?)) + } else { + Ok(Some(self.parse_array_or_builtin_call(vref, token_span.pos)?)) + } + } + Token::Bad(msg) => Err(Error::Bad(token_span.pos, msg)), + t => Err(Error::Bad(token_span.pos, format!("Unexpected {} in uniline IF branch", t))), + } + } + + /// Extracts the next available statement from the input stream, or `None` if none is available. + /// + /// On success, the stream is left in a position where the next statement can be extracted. + /// On failure, the caller must advance the stream to the next statement by calling `reset`. + fn parse_one(&mut self) -> Result> { + loop { + match self.lexer.peek()?.token { + Token::Eol => { + self.lexer.consume_peeked(); + } + Token::Eof => return Ok(None), + _ => break, + } + } + let token_span = self.lexer.read()?; + let res = match token_span.token { + Token::Data => Ok(Some(self.parse_data()?)), + Token::Dim => Ok(Some(self.parse_dim()?)), + Token::Do => { + let result = self.parse_do(token_span.pos); + if result.is_err() { + self.reset_do()?; + } + Ok(Some(result?)) + } + Token::End => Ok(Some(self.parse_end(token_span.pos)?)), + Token::Eof => return Ok(None), + Token::Eol => Ok(None), + Token::Exit => Ok(Some(self.parse_exit(token_span.pos)?)), + Token::If => { + let result = self.parse_if(token_span.pos); + if result.is_err() { + self.reset_if(token_span.pos)?; + } + Ok(Some(result?)) + } + Token::For => { + let result = self.parse_for(token_span.pos); + if result.is_err() { + self.reset_for()?; + } + Ok(Some(result?)) + } + Token::Function => { + let result = self.parse_function(token_span.pos); + if result.is_err() { + self.reset_callable(Token::Function)?; + } + Ok(Some(result?)) + } + Token::Gosub => { + let result = self.parse_gosub(); + Ok(Some(result?)) + } + Token::Goto => { + let result = self.parse_goto(); + Ok(Some(result?)) + } + Token::Integer(i) => { + let name = format!("{}", i); + // When we encounter a line number, we must return early to avoid looking for a line + // ending given that the next statement may start after the label we found. + return Ok(Some(Statement::Label(LabelSpan { name, name_pos: token_span.pos }))); + } + Token::Label(name) => { + // When we encounter a label, we must return early to avoid looking for a line + // ending given that the next statement may start after the label we found. + return Ok(Some(Statement::Label(LabelSpan { name, name_pos: token_span.pos }))); + } + Token::On => Ok(Some(self.parse_on()?)), + Token::Return => Ok(Some(Statement::Return(ReturnSpan { pos: token_span.pos }))), + Token::Select => { + let result = self.parse_select(token_span.pos); + if result.is_err() { + self.reset_select(token_span.pos)?; + } + Ok(Some(result?)) + } + Token::Sub => { + let result = self.parse_sub(token_span.pos); + if result.is_err() { + self.reset_callable(Token::Sub)?; + } + Ok(Some(result?)) + } + Token::Symbol(vref) => { + let peeked = self.lexer.peek()?; + if peeked.token == Token::Equal { + self.lexer.consume_peeked(); + Ok(Some(self.parse_assignment(vref, token_span.pos)?)) + } else { + Ok(Some(self.parse_array_or_builtin_call(vref, token_span.pos)?)) + } + } + Token::While => { + let result = self.parse_while(token_span.pos); + if result.is_err() { + self.reset_while()?; + } + Ok(Some(result?)) + } + Token::Bad(msg) => return Err(Error::Bad(token_span.pos, msg)), + t => return Err(Error::Bad(token_span.pos, format!("Unexpected {} in statement", t))), + }; + + let token_span = self.lexer.peek()?; + match token_span.token { + Token::Eof => (), + Token::Eol => { + self.lexer.consume_peeked(); + } + _ => { + return Err(Error::Bad( + token_span.pos, + format!("Expected newline but found {}", token_span.token), + )); + } + }; + + res + } + + /// Advances until the next statement after failing to parse a single statement. + fn reset(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Eol => { + self.lexer.consume_peeked(); + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + Ok(()) + } + + /// Extracts the next available statement from the input stream, or `None` if none is available. + /// + /// The stream is always left in a position where the next statement extraction can be tried. + fn parse_one_safe(&mut self) -> Result> { + let result = self.parse_one(); + if result.is_err() { + self.reset()?; + } + result + } +} + +pub(crate) struct StatementIter<'a> { + parser: Parser<'a>, +} + +impl Iterator for StatementIter<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + self.parser.parse_one_safe().transpose() + } +} + +/// Extracts all statements from the input stream. +pub(crate) fn parse(input: &mut dyn io::Read) -> StatementIter<'_> { + StatementIter { parser: Parser::from(input) } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::ExprType; + + /// Syntactic sugar to instantiate a `LineCol` for testing. + fn lc(line: usize, col: usize) -> LineCol { + LineCol { line, col } + } + + /// Syntactic sugar to instantiate an `Expr::Boolean` for testing. + fn expr_boolean(value: bool, line: usize, col: usize) -> Expr { + Expr::Boolean(BooleanSpan { value, pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Double` for testing. + fn expr_double(value: f64, line: usize, col: usize) -> Expr { + Expr::Double(DoubleSpan { value, pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Integer` for testing. + fn expr_integer(value: i32, line: usize, col: usize) -> Expr { + Expr::Integer(IntegerSpan { value, pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Text` for testing. + fn expr_text>(value: S, line: usize, col: usize) -> Expr { + Expr::Text(TextSpan { value: value.into(), pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Symbol` for testing. + fn expr_symbol(vref: VarRef, line: usize, col: usize) -> Expr { + Expr::Symbol(SymbolSpan { vref, pos: LineCol { line, col } }) + } + + #[test] + fn test_varref_to_unannotated_string() { + assert_eq!( + "print", + &vref_to_unannotated_string(VarRef::new("print", None), LineCol { line: 0, col: 0 }) + .unwrap() + ); + + assert_eq!( + "7:6: Type annotation not allowed in print$", + format!( + "{}", + &vref_to_unannotated_string( + VarRef::new("print", Some(ExprType::Text)), + LineCol { line: 7, col: 6 } + ) + .unwrap_err() + ) + ); + } + + /// Runs the parser on the given `input` and expects the returned statements to match + /// `exp_statements`. + fn do_ok_test(input: &str, exp_statements: &[Statement]) { + let mut input = input.as_bytes(); + let statements = + parse(&mut input).map(|r| r.expect("Parsing failed")).collect::>(); + assert_eq!(exp_statements, statements.as_slice()); + } + + /// Runs the parser on the given `input` and expects the `err` error message. + fn do_error_test(input: &str, expected_err: &str) { + let mut input = input.as_bytes(); + let mut parser = Parser::from(&mut input); + assert_eq!( + expected_err, + format!("{}", parser.parse_one_safe().expect_err("Parsing did not fail")) + ); + assert!(parser.parse_one_safe().unwrap().is_none()); + } + + /// Runs the parser on the given `input` and expects the `err` error message. + /// + /// Does not expect the parser to be reset to the next (EOF) statement. + // TODO(jmmv): Need better testing to ensure the parser is reset to something that can be + // parsed next. + fn do_error_test_no_reset(input: &str, expected_err: &str) { + let mut input = input.as_bytes(); + for result in parse(&mut input) { + if let Err(e) = result { + assert_eq!(expected_err, format!("{}", e)); + return; + } + } + panic!("Parsing did not fail") + } + + #[test] + fn test_empty() { + do_ok_test("", &[]); + } + + #[test] + fn test_statement_separators() { + do_ok_test( + "a=1\nb=2:c=3:' A comment: that follows\nd=4", + &[ + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 1), + expr: expr_integer(1, 1, 3), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("b", None), + vref_pos: lc(2, 1), + expr: expr_integer(2, 2, 3), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("c", None), + vref_pos: lc(2, 5), + expr: expr_integer(3, 2, 7), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("d", None), + vref_pos: lc(3, 1), + expr: expr_integer(4, 3, 3), + }), + ], + ); + } + + #[test] + fn test_array_assignments() { + do_ok_test( + "a(1)=100\nfoo(2, 3)=\"text\"\nabc$ (5 + z, 6) = TRUE OR FALSE", + &[ + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 1), + subscripts: vec![expr_integer(1, 1, 3)], + expr: expr_integer(100, 1, 6), + }), + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("foo", None), + vref_pos: lc(2, 1), + subscripts: vec![expr_integer(2, 2, 5), expr_integer(3, 2, 8)], + expr: expr_text("text", 2, 11), + }), + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("abc", Some(ExprType::Text)), + vref_pos: lc(3, 1), + subscripts: vec![ + Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_integer(5, 3, 7), + rhs: expr_symbol(VarRef::new("z".to_owned(), None), 3, 11), + pos: lc(3, 9), + })), + expr_integer(6, 3, 14), + ], + expr: Expr::Or(Box::from(BinaryOpSpan { + lhs: expr_boolean(true, 3, 19), + rhs: expr_boolean(false, 3, 27), + pos: lc(3, 24), + })), + }), + ], + ); + } + + #[test] + fn test_array_assignment_errors() { + do_error_test("a(", "1:3: Unexpected <>"); + do_error_test("a()", "1:2: Expected expression"); + do_error_test("a() =", "1:6: Missing expression in array assignment"); + do_error_test("a() IF", "1:2: Expected expression"); + do_error_test("a() = 3 4", "1:9: Unexpected value in expression"); + do_error_test("a() = 3 THEN", "1:9: Unexpected THEN in array assignment"); + do_error_test("a(,) = 3", "1:3: Missing expression"); + do_error_test("a(2;3) = 3", "1:4: Unexpected ;"); + do_error_test("(2) = 3", "1:1: Unexpected ( in statement"); + } + + #[test] + fn test_assignments() { + do_ok_test( + "a=1\nfoo$ = \"bar\"\nb$ = 3 + z", + &[ + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 1), + expr: expr_integer(1, 1, 3), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("foo", Some(ExprType::Text)), + vref_pos: lc(2, 1), + expr: expr_text("bar", 2, 8), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("b", Some(ExprType::Text)), + vref_pos: lc(3, 1), + expr: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 3, 6), + rhs: expr_symbol(VarRef::new("z", None), 3, 10), + pos: lc(3, 8), + })), + }), + ], + ); + } + + #[test] + fn test_assignment_errors() { + do_error_test("a =", "1:4: Missing expression in assignment"); + do_error_test("a = b 3", "1:7: Unexpected value in expression"); + do_error_test("a = b, 3", "1:6: Unexpected , in assignment"); + do_error_test("a = if 3", "1:5: Unexpected keyword in expression"); + do_error_test("true = 1", "1:1: Unexpected TRUE in statement"); + } + + #[test] + fn test_builtin_calls() { + do_ok_test( + "PRINT a\nPRINT ; 3 , c$\nNOARGS\nNAME 3 AS 4", + &[ + Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ArgSpan { + expr: Some(expr_symbol(VarRef::new("a", None), 1, 7)), + sep: ArgSep::End, + sep_pos: lc(1, 8), + }], + }), + Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(2, 1), + args: vec![ + ArgSpan { expr: None, sep: ArgSep::Short, sep_pos: lc(2, 7) }, + ArgSpan { + expr: Some(expr_integer(3, 2, 9)), + sep: ArgSep::Long, + sep_pos: lc(2, 11), + }, + ArgSpan { + expr: Some(expr_symbol(VarRef::new("c", Some(ExprType::Text)), 2, 13)), + sep: ArgSep::End, + sep_pos: lc(2, 15), + }, + ], + }), + Statement::Call(CallSpan { + vref: VarRef::new("NOARGS", None), + vref_pos: lc(3, 1), + args: vec![], + }), + Statement::Call(CallSpan { + vref: VarRef::new("NAME", None), + vref_pos: lc(4, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(3, 4, 6)), + sep: ArgSep::As, + sep_pos: lc(4, 8), + }, + ArgSpan { + expr: Some(expr_integer(4, 4, 11)), + sep: ArgSep::End, + sep_pos: lc(4, 12), + }, + ], + }), + ], + ); + } + + #[test] + fn test_builtin_calls_and_array_references_disambiguation() { + use Expr::*; + + do_ok_test( + "PRINT(1)", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::End, + sep_pos: lc(1, 9), + }], + })], + ); + + do_ok_test( + "PRINT(1), 2", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::Long, + sep_pos: lc(1, 9), + }, + ArgSpan { + expr: Some(expr_integer(2, 1, 11)), + sep: ArgSep::End, + sep_pos: lc(1, 12), + }, + ], + })], + ); + + do_ok_test( + "PRINT(1); 2", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::Short, + sep_pos: lc(1, 9), + }, + ArgSpan { + expr: Some(expr_integer(2, 1, 11)), + sep: ArgSep::End, + sep_pos: lc(1, 12), + }, + ], + })], + ); + + do_ok_test( + "PRINT(1);", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::Short, + sep_pos: lc(1, 9), + }, + ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 10) }, + ], + })], + ); + + do_ok_test( + "PRINT(1) + 2; 3", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(Add(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 10), + }))), + sep: ArgSep::Short, + sep_pos: lc(1, 13), + }, + ArgSpan { + expr: Some(expr_integer(3, 1, 15)), + sep: ArgSep::End, + sep_pos: lc(1, 16), + }, + ], + })], + ); + } + + #[test] + fn test_builtin_calls_errors() { + do_error_test("FOO 3 5\n", "1:7: Unexpected value in expression"); + do_error_test("INPUT$ a\n", "1:1: Type annotation not allowed in INPUT$"); + do_error_test("PRINT IF 1\n", "1:7: Unexpected keyword in expression"); + do_error_test("PRINT 3, IF 1\n", "1:10: Unexpected keyword in expression"); + do_error_test("PRINT 3 THEN\n", "1:9: Expected comma, semicolon, or end of statement"); + do_error_test("PRINT ()\n", "1:7: Expected expression"); + do_error_test("PRINT (2, 3)\n", "1:7: Expected expression"); + do_error_test("PRINT (2, 3); 4\n", "1:7: Expected expression"); + } + + #[test] + fn test_data() { + do_ok_test("DATA", &[Statement::Data(DataSpan { values: vec![None] })]); + + do_ok_test("DATA , ", &[Statement::Data(DataSpan { values: vec![None, None] })]); + do_ok_test( + "DATA , , ,", + &[Statement::Data(DataSpan { values: vec![None, None, None, None] })], + ); + + do_ok_test( + "DATA 1: DATA 2", + &[ + Statement::Data(DataSpan { + values: vec![Some(Expr::Integer(IntegerSpan { value: 1, pos: lc(1, 6) }))], + }), + Statement::Data(DataSpan { + values: vec![Some(Expr::Integer(IntegerSpan { value: 2, pos: lc(1, 14) }))], + }), + ], + ); + + do_ok_test( + "DATA TRUE, -3, 5.1, \"foo\"", + &[Statement::Data(DataSpan { + values: vec![ + Some(Expr::Boolean(BooleanSpan { value: true, pos: lc(1, 6) })), + Some(Expr::Integer(IntegerSpan { value: -3, pos: lc(1, 12) })), + Some(Expr::Double(DoubleSpan { value: 5.1, pos: lc(1, 16) })), + Some(Expr::Text(TextSpan { value: "foo".to_owned(), pos: lc(1, 21) })), + ], + })], + ); + + do_ok_test( + "DATA , TRUE, , 3, , 5.1, , \"foo\",", + &[Statement::Data(DataSpan { + values: vec![ + None, + Some(Expr::Boolean(BooleanSpan { value: true, pos: lc(1, 8) })), + None, + Some(Expr::Integer(IntegerSpan { value: 3, pos: lc(1, 16) })), + None, + Some(Expr::Double(DoubleSpan { value: 5.1, pos: lc(1, 21) })), + None, + Some(Expr::Text(TextSpan { value: "foo".to_owned(), pos: lc(1, 28) })), + None, + ], + })], + ); + + do_ok_test( + "DATA -3, -5.1", + &[Statement::Data(DataSpan { + values: vec![ + Some(Expr::Integer(IntegerSpan { value: -3, pos: lc(1, 6) })), + Some(Expr::Double(DoubleSpan { value: -5.1, pos: lc(1, 10) })), + ], + })], + ); + } + + #[test] + fn test_data_errors() { + do_error_test("DATA + 2", "1:6: Unexpected + in DATA statement"); + do_error_test("DATA ;", "1:6: Unexpected ; in DATA statement"); + do_error_test("DATA 5 + 1", "1:8: Expected comma after datum but found +"); + do_error_test("DATA 5 ; 1", "1:8: Expected comma after datum but found ;"); + do_error_test("DATA -FALSE", "1:6: Expected number after -"); + do_error_test("DATA -\"abc\"", "1:6: Expected number after -"); + do_error_test("DATA -foo", "1:6: Expected number after -"); + } + + #[test] + fn test_dim_default_type() { + do_ok_test( + "DIM i", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(1, 6), + })], + ); + } + + #[test] + fn test_dim_as_simple_types() { + do_ok_test( + "DIM i AS BOOLEAN", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Boolean, + vtype_pos: lc(1, 10), + })], + ); + do_ok_test( + "DIM i AS DOUBLE", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Double, + vtype_pos: lc(1, 10), + })], + ); + do_ok_test( + "DIM i AS INTEGER", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(1, 10), + })], + ); + do_ok_test( + "DIM i AS STRING", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Text, + vtype_pos: lc(1, 10), + })], + ); + } + + #[test] + fn test_dim_consecutive() { + do_ok_test( + "DIM i\nDIM j AS BOOLEAN\nDIM k", + &[ + Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(1, 6), + }), + Statement::Dim(DimSpan { + name: "j".to_owned(), + name_pos: lc(2, 5), + shared: false, + vtype: ExprType::Boolean, + vtype_pos: lc(2, 10), + }), + Statement::Dim(DimSpan { + name: "k".to_owned(), + name_pos: lc(3, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(3, 6), + }), + ], + ); + } + + #[test] + fn test_dim_shared() { + do_ok_test( + "DIM SHARED i", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 12), + shared: true, + vtype: ExprType::Integer, + vtype_pos: lc(1, 13), + })], + ); + do_ok_test( + "DIM SHARED i AS BOOLEAN", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 12), + shared: true, + vtype: ExprType::Boolean, + vtype_pos: lc(1, 17), + })], + ); + } + + #[test] + fn test_dim_array() { + use Expr::*; + + do_ok_test( + "DIM i(10)", + &[Statement::DimArray(DimArraySpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + dimensions: vec![expr_integer(10, 1, 7)], + subtype: ExprType::Integer, + subtype_pos: lc(1, 10), + })], + ); + + do_ok_test( + "DIM foo(-5, 0) AS STRING", + &[Statement::DimArray(DimArraySpan { + name: "foo".to_owned(), + name_pos: lc(1, 5), + shared: false, + dimensions: vec![ + Negate(Box::from(UnaryOpSpan { expr: expr_integer(5, 1, 10), pos: lc(1, 9) })), + expr_integer(0, 1, 13), + ], + subtype: ExprType::Text, + subtype_pos: lc(1, 19), + })], + ); + + do_ok_test( + "DIM foo(bar$() + 3, 8, -1)", + &[Statement::DimArray(DimArraySpan { + name: "foo".to_owned(), + name_pos: lc(1, 5), + shared: false, + dimensions: vec![ + Add(Box::from(BinaryOpSpan { + lhs: Call(CallSpan { + vref: VarRef::new("bar", Some(ExprType::Text)), + vref_pos: lc(1, 9), + args: vec![], + }), + rhs: expr_integer(3, 1, 18), + pos: lc(1, 16), + })), + expr_integer(8, 1, 21), + Negate(Box::from(UnaryOpSpan { expr: expr_integer(1, 1, 25), pos: lc(1, 24) })), + ], + subtype: ExprType::Integer, + subtype_pos: lc(1, 27), + })], + ); + + do_ok_test( + "DIM SHARED i(10)", + &[Statement::DimArray(DimArraySpan { + name: "i".to_owned(), + name_pos: lc(1, 12), + shared: true, + dimensions: vec![expr_integer(10, 1, 14)], + subtype: ExprType::Integer, + subtype_pos: lc(1, 17), + })], + ); + } + + #[test] + fn test_dim_errors() { + do_error_test("DIM", "1:4: Expected variable name after DIM"); + do_error_test("DIM 3", "1:5: Expected variable name after DIM"); + do_error_test("DIM AS", "1:5: Expected variable name after DIM"); + do_error_test("DIM foo 3", "1:9: Expected AS or end of statement"); + do_error_test("DIM a AS", "1:9: Invalid type name <> in AS type definition"); + do_error_test("DIM a$ AS", "1:5: Type annotation not allowed in a$"); + do_error_test("DIM a AS 3", "1:10: Invalid type name 3 in AS type definition"); + do_error_test("DIM a AS INTEGER 3", "1:18: Unexpected 3 in DIM statement"); + + do_error_test("DIM a()", "1:6: Arrays require at least one dimension"); + do_error_test("DIM a(,)", "1:7: Missing expression"); + do_error_test("DIM a(, 3)", "1:7: Missing expression"); + do_error_test("DIM a(3, )", "1:10: Missing expression"); + do_error_test("DIM a(3, , 4)", "1:10: Missing expression"); + do_error_test("DIM a(1) AS INTEGER 3", "1:21: Unexpected 3 in DIM statement"); + } + + #[test] + fn test_do_until_empty() { + do_ok_test( + "DO UNTIL TRUE\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreUntil(expr_boolean(true, 1, 10)), + body: vec![], + })], + ); + + do_ok_test( + "DO UNTIL FALSE\nREM foo\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreUntil(expr_boolean(false, 1, 10)), + body: vec![], + })], + ); + } + + #[test] + fn test_do_infinite_empty() { + do_ok_test("DO\nLOOP", &[Statement::Do(DoSpan { guard: DoGuard::Infinite, body: vec![] })]); + } + + #[test] + fn test_do_pre_until_loops() { + do_ok_test( + "DO UNTIL TRUE\nA\nB\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreUntil(expr_boolean(true, 1, 10)), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_pre_while_loops() { + do_ok_test( + "DO WHILE TRUE\nA\nB\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreWhile(expr_boolean(true, 1, 10)), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_post_until_loops() { + do_ok_test( + "DO\nA\nB\nLOOP UNTIL TRUE", + &[Statement::Do(DoSpan { + guard: DoGuard::PostUntil(expr_boolean(true, 4, 12)), + + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_post_while_loops() { + do_ok_test( + "DO\nA\nB\nLOOP WHILE FALSE", + &[Statement::Do(DoSpan { + guard: DoGuard::PostWhile(expr_boolean(false, 4, 12)), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_nested() { + let code = r#" + DO WHILE TRUE + A + DO + B + LOOP UNTIL FALSE + C + LOOP + "#; + do_ok_test( + code, + &[Statement::Do(DoSpan { + guard: DoGuard::PreWhile(expr_boolean(true, 2, 22)), + body: vec![ + make_bare_builtin_call("A", 3, 17), + Statement::Do(DoSpan { + guard: DoGuard::PostUntil(expr_boolean(false, 6, 28)), + body: vec![make_bare_builtin_call("B", 5, 21)], + }), + make_bare_builtin_call("C", 7, 17), + ], + })], + ); + } + + #[test] + fn test_do_errors() { + do_error_test("DO\n", "1:1: DO without LOOP"); + do_error_test("DO FOR\n", "1:4: Expecting newline, UNTIL or WHILE after DO"); + + do_error_test("\n\nDO UNTIL TRUE\n", "3:1: DO without LOOP"); + do_error_test("\n\nDO WHILE TRUE\n", "3:1: DO without LOOP"); + do_error_test("DO UNTIL TRUE\nEND", "1:1: DO without LOOP"); + do_error_test("DO WHILE TRUE\nEND", "1:1: DO without LOOP"); + do_error_test("DO UNTIL TRUE\nEND\n", "1:1: DO without LOOP"); + do_error_test("DO WHILE TRUE\nEND\n", "1:1: DO without LOOP"); + do_error_test("DO UNTIL TRUE\nEND WHILE\n", "2:5: Unexpected keyword in expression"); + do_error_test("DO WHILE TRUE\nEND WHILE\n", "2:5: Unexpected keyword in expression"); + + do_error_test("DO UNTIL\n", "1:9: No expression in UNTIL clause"); + do_error_test("DO WHILE\n", "1:9: No expression in WHILE clause"); + do_error_test("DO UNTIL TRUE", "1:14: Expecting newline after DO"); + do_error_test("DO WHILE TRUE", "1:14: Expecting newline after DO"); + + do_error_test("DO\nLOOP UNTIL", "2:11: No expression in UNTIL clause"); + do_error_test("DO\nLOOP WHILE\n", "2:11: No expression in WHILE clause"); + + do_error_test("DO UNTIL ,\nLOOP", "1:10: No expression in UNTIL clause"); + do_error_test("DO WHILE ,\nLOOP", "1:10: No expression in WHILE clause"); + + do_error_test("DO\nLOOP UNTIL ,\n", "2:12: No expression in UNTIL clause"); + do_error_test("DO\nLOOP WHILE ,\n", "2:12: No expression in WHILE clause"); + + do_error_test( + "DO WHILE TRUE\nLOOP UNTIL FALSE", + "1:1: DO loop cannot have pre and post guards at the same time", + ); + } + + #[test] + fn test_exit_do() { + do_ok_test(" EXIT DO", &[Statement::ExitDo(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_for() { + do_ok_test(" EXIT FOR", &[Statement::ExitFor(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_function() { + do_ok_test(" EXIT FUNCTION", &[Statement::ExitFunction(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_sub() { + do_ok_test(" EXIT SUB", &[Statement::ExitSub(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_errors() { + do_error_test("EXIT", "1:5: Expecting DO, FOR, FUNCTION or SUB after EXIT"); + do_error_test("EXIT 5", "1:6: Expecting DO, FOR, FUNCTION or SUB after EXIT"); + } + + /// Wrapper around `do_ok_test` to parse an expression. Given that expressions alone are not + /// valid statements, we have to put them in a statement to parse them. In doing so, we can + /// also put an extra statement after them to ensure we detect their end properly. + fn do_expr_ok_test(input: &str, expr: Expr) { + do_ok_test( + &format!("PRINT {}, 1", input), + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr), + sep: ArgSep::Long, + sep_pos: lc(1, 7 + input.len()), + }, + ArgSpan { + expr: Some(expr_integer(1, 1, 6 + input.len() + 3)), + sep: ArgSep::End, + sep_pos: lc(1, 10 + input.len()), + }, + ], + })], + ); + } + + /// Wrapper around `do_error_test` to parse an expression. Given that expressions alone are not + /// valid statements, we have to put them in a statement to parse them. In doing so, we can + /// also put an extra statement after them to ensure we detect their end properly. + fn do_expr_error_test(input: &str, msg: &str) { + do_error_test(&format!("PRINT {}, 1", input), msg) + } + + #[test] + fn test_expr_literals() { + do_expr_ok_test("TRUE", expr_boolean(true, 1, 7)); + do_expr_ok_test("FALSE", expr_boolean(false, 1, 7)); + do_expr_ok_test("5", expr_integer(5, 1, 7)); + do_expr_ok_test("\"some text\"", expr_text("some text", 1, 7)); + } + + #[test] + fn test_expr_symbols() { + do_expr_ok_test("foo", expr_symbol(VarRef::new("foo", None), 1, 7)); + do_expr_ok_test("bar$", expr_symbol(VarRef::new("bar", Some(ExprType::Text)), 1, 7)); + } + + #[test] + fn test_expr_parens() { + use Expr::*; + do_expr_ok_test("(1)", expr_integer(1, 1, 8)); + do_expr_ok_test("((1))", expr_integer(1, 1, 9)); + do_expr_ok_test(" ( ( 1 ) ) ", expr_integer(1, 1, 12)); + do_expr_ok_test( + "3 * (2 + 5)", + Multiply(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 1, 7), + rhs: Add(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 12), + rhs: expr_integer(5, 1, 16), + pos: lc(1, 14), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "(7) - (1) + (-2)", + Add(Box::from(BinaryOpSpan { + lhs: Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(7, 1, 8), + rhs: expr_integer(1, 1, 14), + pos: lc(1, 11), + })), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(2, 1, 21), + pos: lc(1, 20), + })), + pos: lc(1, 17), + })), + ); + } + + #[test] + fn test_expr_arith_ops() { + use Expr::*; + let span = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 11), + pos: lc(1, 9), + }); + do_expr_ok_test("1 + 2", Add(span.clone())); + do_expr_ok_test("1 - 2", Subtract(span.clone())); + do_expr_ok_test("1 * 2", Multiply(span.clone())); + do_expr_ok_test("1 / 2", Divide(span.clone())); + do_expr_ok_test("1 ^ 2", Power(span)); + let span = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 13), + pos: lc(1, 9), + }); + do_expr_ok_test("1 MOD 2", Modulo(span)); + } + + #[test] + fn test_expr_rel_ops() { + use Expr::*; + let span1 = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 11), + pos: lc(1, 9), + }); + let span2 = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + }); + do_expr_ok_test("1 = 2", Equal(span1.clone())); + do_expr_ok_test("1 <> 2", NotEqual(span2.clone())); + do_expr_ok_test("1 < 2", Less(span1.clone())); + do_expr_ok_test("1 <= 2", LessEqual(span2.clone())); + do_expr_ok_test("1 > 2", Greater(span1)); + do_expr_ok_test("1 >= 2", GreaterEqual(span2)); + } + + #[test] + fn test_expr_logical_ops() { + use Expr::*; + do_expr_ok_test( + "1 AND 2", + And(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 13), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "1 OR 2", + Or(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "1 XOR 2", + Xor(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 13), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_logical_ops_not() { + use Expr::*; + do_expr_ok_test( + "NOT TRUE", + Not(Box::from(UnaryOpSpan { expr: expr_boolean(true, 1, 11), pos: lc(1, 7) })), + ); + do_expr_ok_test( + "NOT 6", + Not(Box::from(UnaryOpSpan { expr: expr_integer(6, 1, 11), pos: lc(1, 7) })), + ); + do_expr_ok_test( + "NOT NOT TRUE", + Not(Box::from(UnaryOpSpan { + expr: Not(Box::from(UnaryOpSpan { + expr: expr_boolean(true, 1, 15), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + do_expr_ok_test( + "1 - NOT 4", + Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: Not(Box::from(UnaryOpSpan { expr: expr_integer(4, 1, 15), pos: lc(1, 11) })), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_bitwise_ops() { + use Expr::*; + do_expr_ok_test( + "1 << 2", + ShiftLeft(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "1 >> 2", + ShiftRight(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_op_priorities() { + use Expr::*; + do_expr_ok_test( + "3 * (2 + 5) = (3 + 1 = 2 OR 1 = 3 XOR FALSE * \"a\")", + Equal(Box::from(BinaryOpSpan { + lhs: Multiply(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 1, 7), + rhs: Add(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 12), + rhs: expr_integer(5, 1, 16), + pos: lc(1, 14), + })), + pos: lc(1, 9), + })), + rhs: Xor(Box::from(BinaryOpSpan { + lhs: Or(Box::from(BinaryOpSpan { + lhs: Equal(Box::from(BinaryOpSpan { + lhs: Add(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 1, 22), + rhs: expr_integer(1, 1, 26), + pos: lc(1, 24), + })), + rhs: expr_integer(2, 1, 30), + pos: lc(1, 28), + })), + rhs: Equal(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 35), + rhs: expr_integer(3, 1, 39), + pos: lc(1, 37), + })), + pos: lc(1, 32), + })), + rhs: Multiply(Box::from(BinaryOpSpan { + lhs: expr_boolean(false, 1, 45), + rhs: expr_text("a", 1, 53), + pos: lc(1, 51), + })), + pos: lc(1, 41), + })), + pos: lc(1, 19), + })), + ); + do_expr_ok_test( + "-1 ^ 3", + Negate(Box::from(UnaryOpSpan { + expr: Power(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 8), + rhs: expr_integer(3, 1, 12), + pos: lc(1, 10), + })), + pos: lc(1, 7), + })), + ); + do_expr_ok_test( + "-(1 ^ 3)", + Negate(Box::from(UnaryOpSpan { + expr: Power(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 9), + rhs: expr_integer(3, 1, 13), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + do_expr_ok_test( + "(-1) ^ 3", + Power(Box::from(BinaryOpSpan { + lhs: Negate(Box::from(UnaryOpSpan { expr: expr_integer(1, 1, 9), pos: lc(1, 8) })), + rhs: expr_integer(3, 1, 14), + pos: lc(1, 12), + })), + ); + do_expr_ok_test( + "1 ^ (-3)", + Power(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 13), + pos: lc(1, 12), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "0 <> 2 >> 1", + NotEqual(Box::from(BinaryOpSpan { + lhs: expr_integer(0, 1, 7), + rhs: ShiftRight(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 12), + rhs: expr_integer(1, 1, 17), + pos: lc(1, 14), + })), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_numeric_signs() { + use Expr::*; + + do_expr_ok_test( + "-a", + Negate(Box::from(UnaryOpSpan { + expr: expr_symbol(VarRef::new("a", None), 1, 8), + pos: lc(1, 7), + })), + ); + + do_expr_ok_test( + "1 - -3", + Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "-1 - 3", + Subtract(Box::from(BinaryOpSpan { + lhs: Negate(Box::from(UnaryOpSpan { expr: expr_integer(1, 1, 8), pos: lc(1, 7) })), + rhs: expr_integer(3, 1, 12), + pos: lc(1, 10), + })), + ); + do_expr_ok_test( + "5 + -1", + Add(Box::from(BinaryOpSpan { + lhs: expr_integer(5, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(1, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "-5 + 1", + Add(Box::from(BinaryOpSpan { + lhs: Negate(Box::from(UnaryOpSpan { expr: expr_integer(5, 1, 8), pos: lc(1, 7) })), + rhs: expr_integer(1, 1, 12), + pos: lc(1, 10), + })), + ); + do_expr_ok_test( + "NOT -3", + Not(Box::from(UnaryOpSpan { + expr: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + + do_expr_ok_test( + "1.0 - -3.5", + Subtract(Box::from(BinaryOpSpan { + lhs: expr_double(1.0, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_double(3.5, 1, 14), + pos: lc(1, 13), + })), + pos: lc(1, 11), + })), + ); + do_expr_ok_test( + "5.12 + -0.50", + Add(Box::from(BinaryOpSpan { + lhs: expr_double(5.12, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_double(0.50, 1, 15), + pos: lc(1, 14), + })), + pos: lc(1, 12), + })), + ); + do_expr_ok_test( + "NOT -3", + Not(Box::from(UnaryOpSpan { + expr: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + } + + #[test] + fn test_expr_functions_variadic() { + use Expr::*; + do_expr_ok_test( + "zero()", + Call(CallSpan { vref: VarRef::new("zero", None), vref_pos: lc(1, 7), args: vec![] }), + ); + do_expr_ok_test( + "one%(1)", + Call(CallSpan { + vref: VarRef::new("one", Some(ExprType::Integer)), + vref_pos: lc(1, 7), + args: vec![ArgSpan { + expr: Some(expr_integer(1, 1, 12)), + sep: ArgSep::End, + sep_pos: lc(1, 13), + }], + }), + ); + do_expr_ok_test( + "many$(3, \"x\", TRUE)", + Call(CallSpan { + vref: VarRef::new("many", Some(ExprType::Text)), + vref_pos: lc(1, 7), + args: vec![ + ArgSpan { + expr: Some(expr_integer(3, 1, 13)), + sep: ArgSep::Long, + sep_pos: lc(1, 14), + }, + ArgSpan { + expr: Some(expr_text("x", 1, 16)), + sep: ArgSep::Long, + sep_pos: lc(1, 19), + }, + ArgSpan { + expr: Some(expr_boolean(true, 1, 21)), + sep: ArgSep::End, + sep_pos: lc(1, 25), + }, + ], + }), + ); + } + + #[test] + fn test_expr_functions_nested() { + use Expr::*; + do_expr_ok_test( + "consecutive(parenthesis())", + Call(CallSpan { + vref: VarRef::new("consecutive", None), + vref_pos: lc(1, 7), + args: vec![ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("parenthesis", None), + vref_pos: lc(1, 19), + args: vec![], + })), + sep: ArgSep::End, + sep_pos: lc(1, 32), + }], + }), + ); + do_expr_ok_test( + "outer?(1, inner1(2, 3), 4, inner2(), 5)", + Call(CallSpan { + vref: VarRef::new("outer", Some(ExprType::Boolean)), + vref_pos: lc(1, 7), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 14)), + sep: ArgSep::Long, + sep_pos: lc(1, 15), + }, + ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("inner1", None), + vref_pos: lc(1, 17), + args: vec![ + ArgSpan { + expr: Some(expr_integer(2, 1, 24)), + sep: ArgSep::Long, + sep_pos: lc(1, 25), + }, + ArgSpan { + expr: Some(expr_integer(3, 1, 27)), + sep: ArgSep::End, + sep_pos: lc(1, 28), + }, + ], + })), + sep: ArgSep::Long, + sep_pos: lc(1, 29), + }, + ArgSpan { + expr: Some(expr_integer(4, 1, 31)), + sep: ArgSep::Long, + sep_pos: lc(1, 32), + }, + ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("inner2", None), + vref_pos: lc(1, 34), + args: vec![], + })), + sep: ArgSep::Long, + sep_pos: lc(1, 42), + }, + ArgSpan { + expr: Some(expr_integer(5, 1, 44)), + sep: ArgSep::End, + sep_pos: lc(1, 45), + }, + ], + }), + ); + } + + #[test] + fn test_expr_functions_and_ops() { + use Expr::*; + do_expr_ok_test( + "b AND ask?(34 + 15, ask(1, FALSE), -5)", + And(Box::from(BinaryOpSpan { + lhs: expr_symbol(VarRef::new("b".to_owned(), None), 1, 7), + rhs: Call(CallSpan { + vref: VarRef::new("ask", Some(ExprType::Boolean)), + vref_pos: lc(1, 13), + args: vec![ + ArgSpan { + expr: Some(Add(Box::from(BinaryOpSpan { + lhs: expr_integer(34, 1, 18), + rhs: expr_integer(15, 1, 23), + pos: lc(1, 21), + }))), + sep: ArgSep::Long, + sep_pos: lc(1, 25), + }, + ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("ask", None), + vref_pos: lc(1, 27), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 31)), + sep: ArgSep::Long, + sep_pos: lc(1, 32), + }, + ArgSpan { + expr: Some(expr_boolean(false, 1, 34)), + sep: ArgSep::End, + sep_pos: lc(1, 39), + }, + ], + })), + sep: ArgSep::Long, + sep_pos: lc(1, 40), + }, + ArgSpan { + expr: Some(Negate(Box::from(UnaryOpSpan { + expr: expr_integer(5, 1, 43), + pos: lc(1, 42), + }))), + sep: ArgSep::End, + sep_pos: lc(1, 44), + }, + ], + }), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_functions_not_confused_with_symbols() { + use Expr::*; + let iref = VarRef::new("i", None); + let jref = VarRef::new("j", None); + do_expr_ok_test( + "i = 0 OR i = (j - 1)", + Or(Box::from(BinaryOpSpan { + lhs: Equal(Box::from(BinaryOpSpan { + lhs: expr_symbol(iref.clone(), 1, 7), + rhs: expr_integer(0, 1, 11), + pos: lc(1, 9), + })), + rhs: Equal(Box::from(BinaryOpSpan { + lhs: expr_symbol(iref, 1, 16), + rhs: Subtract(Box::from(BinaryOpSpan { + lhs: expr_symbol(jref, 1, 21), + rhs: expr_integer(1, 1, 25), + pos: lc(1, 23), + })), + pos: lc(1, 18), + })), + pos: lc(1, 13), + })), + ); + } + + #[test] + fn test_expr_errors() { + // Note that all column numbers in the errors below are offset by 6 (due to "PRINT ") as + // that's what the do_expr_error_test function is prefixing to the given strings. + + do_expr_error_test("+3", "1:7: Not enough values to apply operator"); + do_expr_error_test("2 + * 3", "1:9: Not enough values to apply operator"); + do_expr_error_test("(2(3))", "1:9: Unexpected ( in expression"); + do_expr_error_test("((3)2)", "1:11: Unexpected value in expression"); + do_expr_error_test("2 3", "1:9: Unexpected value in expression"); + + do_expr_error_test("(", "1:8: Missing expression"); + + do_expr_error_test(")", "1:7: Expected comma, semicolon, or end of statement"); + do_expr_error_test("(()", "1:10: Missing expression"); + do_expr_error_test("())", "1:7: Expected expression"); + do_expr_error_test("3 + (2 + 1) + (4 - 5", "1:21: Unbalanced parenthesis"); + do_expr_error_test( + "3 + 2 + 1) + (4 - 5)", + "1:16: Expected comma, semicolon, or end of statement", + ); + + do_expr_error_test("foo(,)", "1:11: Missing expression"); + do_expr_error_test("foo(, 3)", "1:11: Missing expression"); + do_expr_error_test("foo(3, )", "1:14: Missing expression"); + do_expr_error_test("foo(3, , 4)", "1:14: Missing expression"); + // TODO(jmmv): These are not the best error messages... + do_expr_error_test("(,)", "1:8: Missing expression"); + do_expr_error_test("(3, 4)", "1:7: Expected expression"); + do_expr_error_test("((), ())", "1:10: Missing expression"); + + // TODO(jmmv): This succeeds because `PRINT` is interned as a `Token::Symbol` so the + // expression parser sees it as a variable reference... but this should probably fail. + // Would need to intern builtin call names as a separate token to catch this, but that + // also means that the lexer must be aware of builtin call names upfront. + use Expr::*; + do_expr_ok_test( + "1 + PRINT", + Add(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_symbol(VarRef::new("PRINT", None), 1, 11), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_errors_due_to_keywords() { + for kw in &[ + "BOOLEAN", "CASE", "DATA", "DIM", "DOUBLE", "ELSEIF", "END", "ERROR", "EXIT", "FOR", + "GOSUB", "GOTO", "IF", "IS", "INTEGER", "LOOP", "NEXT", "ON", "RESUME", "RETURN", + "SELECT", "STRING", "UNTIL", "WEND", "WHILE", + ] { + do_expr_error_test( + &format!("2 + {} - 1", kw), + "1:11: Unexpected keyword in expression", + ); + } + } + + #[test] + fn test_if_empty_branches() { + do_ok_test( + "IF 1 THEN\nEND IF", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }], + })], + ); + do_ok_test( + "IF 1 THEN\nREM Some comment to skip over\n\nEND IF", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }], + })], + ); + do_ok_test( + "IF 1 THEN\nELSEIF 2 THEN\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_integer(2, 2, 8), body: vec![] }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\nELSEIF 2 THEN\nELSE\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_integer(2, 2, 8), body: vec![] }, + IfBranchSpan { guard: expr_boolean(true, 3, 1), body: vec![] }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\nELSE\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_boolean(true, 2, 1), body: vec![] }, + ], + })], + ); + } + + /// Helper to instantiate a trivial `Statement::BuiltinCall` that has no arguments. + fn make_bare_builtin_call(name: &str, line: usize, col: usize) -> Statement { + Statement::Call(CallSpan { + vref: VarRef::new(name, None), + vref_pos: LineCol { line, col }, + args: vec![], + }) + } + + #[test] + fn test_if_with_one_statement_or_empty_lines() { + do_ok_test( + "IF 1 THEN\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("PRINT", 2, 1)], + }], + })], + ); + do_ok_test( + "IF 1 THEN\nREM foo\nELSEIF 2 THEN\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { + guard: expr_integer(2, 3, 8), + body: vec![make_bare_builtin_call("PRINT", 4, 1)], + }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\nELSEIF 2 THEN\nELSE\n\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_integer(2, 2, 8), body: vec![] }, + IfBranchSpan { + guard: expr_boolean(true, 3, 1), + body: vec![make_bare_builtin_call("PRINT", 5, 1)], + }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\n\n\nELSE\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { + guard: expr_boolean(true, 4, 1), + body: vec![make_bare_builtin_call("PRINT", 5, 1)], + }, + ], + })], + ); + } + + #[test] + fn test_if_complex() { + let code = r#" + IF 1 THEN 'First branch + A + B + ELSEIF 2 THEN 'Second branch + C + D + ELSEIF 3 THEN 'Third branch + E + F + ELSE 'Last branch + G + H + END IF + "#; + do_ok_test( + code, + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 2, 16), + body: vec![ + make_bare_builtin_call("A", 3, 17), + make_bare_builtin_call("B", 4, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(2, 5, 20), + body: vec![ + make_bare_builtin_call("C", 6, 17), + make_bare_builtin_call("D", 7, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(3, 8, 20), + body: vec![ + make_bare_builtin_call("E", 9, 17), + make_bare_builtin_call("F", 10, 17), + ], + }, + IfBranchSpan { + guard: expr_boolean(true, 11, 13), + body: vec![ + make_bare_builtin_call("G", 12, 17), + make_bare_builtin_call("H", 13, 17), + ], + }, + ], + })], + ); + } + + #[test] + fn test_if_with_interleaved_end_complex() { + let code = r#" + IF 1 THEN 'First branch + A + END + B + ELSEIF 2 THEN 'Second branch + C + END 8 + D + ELSEIF 3 THEN 'Third branch + E + END + F + ELSE 'Last branch + G + END 5 + H + END IF + "#; + do_ok_test( + code, + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 2, 16), + body: vec![ + make_bare_builtin_call("A", 3, 17), + Statement::End(EndSpan { code: None }), + make_bare_builtin_call("B", 5, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(2, 6, 20), + body: vec![ + make_bare_builtin_call("C", 7, 17), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(8, 21) })), + }), + make_bare_builtin_call("D", 9, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(3, 10, 20), + body: vec![ + make_bare_builtin_call("E", 11, 17), + Statement::End(EndSpan { code: None }), + make_bare_builtin_call("F", 13, 17), + ], + }, + IfBranchSpan { + guard: expr_boolean(true, 14, 13), + body: vec![ + make_bare_builtin_call("G", 15, 17), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { + value: 5, + pos: lc(16, 21), + })), + }), + make_bare_builtin_call("H", 17, 17), + ], + }, + ], + })], + ); + } + + #[test] + fn test_if_nested() { + let code = r#" + IF 1 THEN + A + ELSEIF 2 THEN + IF 3 THEN + B + END IF + END IF + "#; + do_ok_test( + code, + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 2, 16), + body: vec![make_bare_builtin_call("A", 3, 17)], + }, + IfBranchSpan { + guard: expr_integer(2, 4, 20), + body: vec![Statement::If(IfSpan { + branches: vec![IfBranchSpan { + guard: expr_integer(3, 5, 20), + body: vec![make_bare_builtin_call("B", 6, 21)], + }], + })], + }, + ], + })], + ); + } + + #[test] + fn test_if_errors() { + do_error_test("IF\n", "1:3: No expression in IF statement"); + do_error_test("IF 3 + 1", "1:9: No THEN in IF statement"); + do_error_test("IF 3 + 1\n", "1:9: No THEN in IF statement"); + do_error_test("IF 3 + 1 PRINT foo\n", "1:10: Unexpected value in expression"); + do_error_test("IF 3 + 1\nPRINT foo\n", "1:9: No THEN in IF statement"); + do_error_test("IF 3 + 1 THEN", "1:1: IF without END IF"); + + do_error_test("IF 1 THEN\n", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nELSEIF 1 THEN\n", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nELSE\n", "1:1: IF without END IF"); + do_error_test("REM\n IF 1 THEN\n", "2:4: IF without END IF"); + + do_error_test("IF 1 THEN\nELSEIF\n", "2:7: No expression in ELSEIF statement"); + do_error_test("IF 1 THEN\nELSEIF 3 + 1", "2:13: No THEN in ELSEIF statement"); + do_error_test("IF 1 THEN\nELSEIF 3 + 1\n", "2:13: No THEN in ELSEIF statement"); + do_error_test( + "IF 1 THEN\nELSEIF 3 + 1 PRINT foo\n", + "2:14: Unexpected value in expression", + ); + do_error_test("IF 1 THEN\nELSEIF 3 + 1\nPRINT foo\n", "2:13: No THEN in ELSEIF statement"); + do_error_test("IF 1 THEN\nELSEIF 3 + 1 THEN", "2:18: Expecting newline after THEN"); + + do_error_test("IF 1 THEN\nELSE", "2:5: Expecting newline after ELSE"); + do_error_test("IF 1 THEN\nELSE foo", "2:6: Expecting newline after ELSE"); + + do_error_test("IF 1 THEN\nEND", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nEND\n", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nEND IF foo", "2:8: Expected newline but found foo"); + do_error_test("IF 1 THEN\nEND SELECT\n", "2:1: END SELECT without SELECT"); + do_error_test("IF 1 THEN\nEND SELECT\nEND IF\n", "2:1: END SELECT without SELECT"); + + do_error_test( + "IF 1 THEN\nELSE\nELSEIF 2 THEN\nEND IF", + "3:1: Unexpected ELSEIF after ELSE", + ); + do_error_test("IF 1 THEN\nELSE\nELSE\nEND IF", "3:1: Duplicate ELSE after ELSE"); + + do_error_test_no_reset("ELSEIF 1 THEN\nEND IF", "1:1: Unexpected ELSEIF in statement"); + do_error_test_no_reset("ELSE 1\nEND IF", "1:1: Unexpected ELSE in statement"); + + do_error_test("IF 1 THEN\nEND 3 IF", "2:7: Unexpected keyword in expression"); + do_error_test("END 3 IF", "1:7: Unexpected keyword in expression"); + do_error_test("END IF", "1:1: END IF without IF"); + + do_error_test("IF TRUE THEN PRINT ELSE ELSE", "1:25: Unexpected ELSE in uniline IF branch"); + } + + #[test] + fn test_if_uniline_then() { + do_ok_test( + "IF 1 THEN A", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("A", 1, 11)], + }], + })], + ); + } + + #[test] + fn test_if_uniline_then_else() { + do_ok_test( + "IF 1 THEN A ELSE B", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("A", 1, 11)], + }, + IfBranchSpan { + guard: expr_boolean(true, 1, 13), + body: vec![make_bare_builtin_call("B", 1, 18)], + }, + ], + })], + ); + } + + #[test] + fn test_if_uniline_empty_then_else() { + do_ok_test( + "IF 1 THEN ELSE B", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { + guard: expr_boolean(true, 1, 11), + body: vec![make_bare_builtin_call("B", 1, 16)], + }, + ], + })], + ); + } + + #[test] + fn test_if_uniline_then_empty_else() { + do_ok_test( + "IF 1 THEN A ELSE", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("A", 1, 11)], + }, + IfBranchSpan { guard: expr_boolean(true, 1, 13), body: vec![] }, + ], + })], + ); + } + + #[test] + fn test_if_uniline_empty_then_empty_else() { + do_ok_test( + "IF 1 THEN ELSE", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_boolean(true, 1, 11), body: vec![] }, + ], + })], + ); + } + + /// Performs a test of a uniline if statement followed by a specific allowed statement type. + /// + /// `text` is the literal statement to append to the if statement, and `stmt` contains the + /// expected parsed statement. The expected positions for the parsed statement are line 1 and + /// columns offset by 11 characters. + fn do_if_uniline_allowed_test(text: &str, stmt: Statement) { + do_ok_test( + &format!("IF 1 THEN {}\nZ", text), + &[ + Statement::If(IfSpan { + branches: vec![IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![stmt] }], + }), + make_bare_builtin_call("Z", 2, 1), + ], + ); + } + + #[test] + fn test_if_uniline_allowed_data() { + do_if_uniline_allowed_test("DATA", Statement::Data(DataSpan { values: vec![None] })); + } + + #[test] + fn test_if_uniline_allowed_end() { + do_if_uniline_allowed_test( + "END 8", + Statement::End(EndSpan { code: Some(expr_integer(8, 1, 15)) }), + ); + } + + #[test] + fn test_if_uniline_allowed_exit() { + do_if_uniline_allowed_test("EXIT DO", Statement::ExitDo(ExitSpan { pos: lc(1, 11) })); + + do_error_test("IF 1 THEN EXIT", "1:15: Expecting DO, FOR, FUNCTION or SUB after EXIT"); + } + + #[test] + fn test_if_uniline_allowed_gosub() { + do_if_uniline_allowed_test( + "GOSUB 10", + Statement::Gosub(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 17) }), + ); + + do_error_test("IF 1 THEN GOSUB", "1:16: Expected label name after GOSUB"); + } + + #[test] + fn test_if_uniline_allowed_goto() { + do_if_uniline_allowed_test( + "GOTO 10", + Statement::Goto(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 16) }), + ); + + do_error_test("IF 1 THEN GOTO", "1:15: Expected label name after GOTO"); + } + + #[test] + fn test_if_uniline_allowed_on_error() { + do_if_uniline_allowed_test( + "ON ERROR RESUME NEXT", + Statement::OnError(OnErrorSpan::ResumeNext), + ); + + do_error_test("IF 1 THEN ON", "1:13: Expected ERROR after ON"); + } + + #[test] + fn test_if_uniline_allowed_return() { + do_if_uniline_allowed_test("RETURN", Statement::Return(ReturnSpan { pos: lc(1, 11) })); + } + + #[test] + fn test_if_uniline_allowed_assignment() { + do_if_uniline_allowed_test( + "a = 3", + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 11), + expr: expr_integer(3, 1, 15), + }), + ); + } + + #[test] + fn test_if_uniline_allowed_array_assignment() { + do_if_uniline_allowed_test( + "a(3) = 5", + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 11), + subscripts: vec![expr_integer(3, 1, 13)], + expr: expr_integer(5, 1, 18), + }), + ); + } + + #[test] + fn test_if_uniline_allowed_builtin_call() { + do_if_uniline_allowed_test( + "a 0", + Statement::Call(CallSpan { + vref: VarRef::new("A", None), + vref_pos: lc(1, 11), + args: vec![ArgSpan { + expr: Some(expr_integer(0, 1, 13)), + sep: ArgSep::End, + sep_pos: lc(1, 14), + }], + }), + ); + } + + #[test] + fn test_if_uniline_unallowed_statements() { + for t in ["DIM", "DO", "IF", "FOR", "10", "@label", "SELECT", "WHILE"] { + do_error_test( + &format!("IF 1 THEN {}", t), + &format!("1:11: Unexpected {} in uniline IF branch", t), + ); + } + } + + #[test] + fn test_for_empty() { + let auto_iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 1 TO 10\nNEXT", + &[Statement::For(ForSpan { + iter: auto_iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(1, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(auto_iter.clone(), 1, 5), + rhs: expr_integer(10, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(auto_iter, 1, 5), + rhs: expr_integer(1, 1, 16), + pos: lc(1, 11), + })), + body: vec![], + })], + ); + + let typed_iter = VarRef::new("d", Some(ExprType::Double)); + do_ok_test( + "FOR d# = 1.0 TO 10.2\nREM Nothing to do\nNEXT", + &[Statement::For(ForSpan { + iter: typed_iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_double(1.0, 1, 10), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(typed_iter.clone(), 1, 5), + rhs: expr_double(10.2, 1, 17), + pos: lc(1, 14), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(typed_iter, 1, 5), + rhs: expr_integer(1, 1, 21), + pos: lc(1, 14), + })), + body: vec![], + })], + ); + } + + #[test] + fn test_for_incrementing() { + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 0 TO 5\nA\nB\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(0, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(5, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_integer(1, 1, 15), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_for_incrementing_with_step() { + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 0 TO 5 STEP 2\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(0, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(5, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_integer(2, 1, 21), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 0 TO 5 STEP 2.5\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: true, + start: expr_integer(0, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(5, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_double(2.5, 1, 21), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + } + + #[test] + fn test_for_decrementing_with_step() { + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 5 TO 0 STEP -1\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(5, 1, 9), + end: Expr::GreaterEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(0, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_integer(-1, 1, 22), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 5 TO 0 STEP -1.2\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: true, + start: expr_integer(5, 1, 9), + end: Expr::GreaterEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(0, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_double(-1.2, 1, 22), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + } + + #[test] + fn test_for_errors() { + do_error_test("FOR\n", "1:4: No iterator name in FOR statement"); + do_error_test("FOR =\n", "1:5: No iterator name in FOR statement"); + do_error_test( + "FOR a$\n", + "1:5: Iterator name in FOR statement must be a numeric reference", + ); + + do_error_test("FOR d#\n", "1:7: No equal sign in FOR statement"); + do_error_test("FOR i 3\n", "1:7: No equal sign in FOR statement"); + do_error_test("FOR i = TO\n", "1:9: No start expression in FOR statement"); + do_error_test("FOR i = NEXT\n", "1:9: Unexpected keyword in expression"); + + do_error_test("FOR i = 3 STEP\n", "1:11: No TO in FOR statement"); + do_error_test("FOR i = 3 TO STEP\n", "1:14: No end expression in FOR statement"); + do_error_test("FOR i = 3 TO NEXT\n", "1:14: Unexpected keyword in expression"); + + do_error_test("FOR i = 3 TO 1 STEP a\n", "1:21: STEP needs a literal number"); + do_error_test("FOR i = 3 TO 1 STEP -a\n", "1:22: STEP needs a literal number"); + do_error_test("FOR i = 3 TO 1 STEP NEXT\n", "1:21: STEP needs a literal number"); + do_error_test("FOR i = 3 TO 1 STEP 0\n", "1:21: Infinite FOR loop; STEP cannot be 0"); + do_error_test("FOR i = 3 TO 1 STEP 0.0\n", "1:21: Infinite FOR loop; STEP cannot be 0"); + + do_error_test("FOR i = 3 TO 1", "1:15: Expecting newline after FOR"); + do_error_test("FOR i = 1 TO 3 STEP 1", "1:22: Expecting newline after FOR"); + do_error_test("FOR i = 3 TO 1 STEP -1", "1:23: Expecting newline after FOR"); + + do_error_test(" FOR i = 0 TO 10\nPRINT i\n", "1:5: FOR without NEXT"); + } + + #[test] + fn test_function_empty() { + do_ok_test( + "FUNCTION foo$\nEND FUNCTION", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 10), + params: vec![], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_function_some_content() { + do_ok_test( + r#" + FUNCTION foo$ + A + END + END 8 + B + END FUNCTION + "#, + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(2, 26), + params: vec![], + body: vec![ + make_bare_builtin_call("A", 3, 21), + Statement::End(EndSpan { code: None }), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(5, 25) })), + }), + make_bare_builtin_call("B", 6, 21), + ], + end_pos: lc(7, 17), + })], + ); + } + + #[test] + fn test_function_one_param() { + do_ok_test( + "FUNCTION foo$(x)\nEND FUNCTION", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 10), + params: vec![VarRef::new("x", None)], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_function_multiple_params() { + do_ok_test( + "FUNCTION foo$(x$, y, z AS BOOLEAN)\nEND FUNCTION", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 10), + params: vec![ + VarRef::new("x", Some(ExprType::Text)), + VarRef::new("y", None), + VarRef::new("z", Some(ExprType::Boolean)), + ], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_function_errors() { + do_error_test("FUNCTION", "1:9: Expected a function name after FUNCTION"); + do_error_test("FUNCTION foo", "1:13: Expected newline after FUNCTION name"); + do_error_test("FUNCTION foo 3", "1:14: Expected newline after FUNCTION name"); + do_error_test("FUNCTION foo\nEND", "1:1: FUNCTION without END FUNCTION"); + do_error_test("FUNCTION foo\nEND IF", "2:1: END IF without IF"); + do_error_test("FUNCTION foo\nEND SUB", "2:1: END SUB without SUB"); + do_error_test( + "FUNCTION foo\nFUNCTION bar\nEND FUNCTION\nEND FUNCTION", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test( + "FUNCTION foo\nSUB bar\nEND SUB\nEND FUNCTION", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test("FUNCTION foo (", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo ()", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo (,)", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo (a,)", "1:17: Expected a parameter name"); + do_error_test("FUNCTION foo (,b)", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo (a AS)", "1:19: Invalid type name ) in AS type definition"); + do_error_test( + "FUNCTION foo (a INTEGER)", + "1:17: Expected comma, AS, or end of parameters list", + ); + do_error_test("FUNCTION foo (a? AS BOOLEAN)", "1:15: Type annotation not allowed in a?"); + } + + #[test] + fn test_gosub_ok() { + do_ok_test( + "GOSUB 10", + &[Statement::Gosub(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 7) })], + ); + + do_ok_test( + "GOSUB @foo", + &[Statement::Gosub(GotoSpan { target: "foo".to_owned(), target_pos: lc(1, 7) })], + ); + } + + #[test] + fn test_gosub_errors() { + do_error_test("GOSUB\n", "1:6: Expected label name after GOSUB"); + do_error_test("GOSUB foo\n", "1:7: Expected label name after GOSUB"); + do_error_test("GOSUB \"foo\"\n", "1:7: Expected label name after GOSUB"); + do_error_test("GOSUB @foo, @bar\n", "1:11: Expected newline but found ,"); + do_error_test("GOSUB @foo, 3\n", "1:11: Expected newline but found ,"); + } + + #[test] + fn test_goto_ok() { + do_ok_test( + "GOTO 10", + &[Statement::Goto(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 6) })], + ); + + do_ok_test( + "GOTO @foo", + &[Statement::Goto(GotoSpan { target: "foo".to_owned(), target_pos: lc(1, 6) })], + ); + } + + #[test] + fn test_goto_errors() { + do_error_test("GOTO\n", "1:5: Expected label name after GOTO"); + do_error_test("GOTO foo\n", "1:6: Expected label name after GOTO"); + do_error_test("GOTO \"foo\"\n", "1:6: Expected label name after GOTO"); + do_error_test("GOTO @foo, @bar\n", "1:10: Expected newline but found ,"); + do_error_test("GOTO @foo, 3\n", "1:10: Expected newline but found ,"); + } + + #[test] + fn test_label_own_line() { + do_ok_test( + "@foo\nPRINT", + &[ + Statement::Label(LabelSpan { name: "foo".to_owned(), name_pos: lc(1, 1) }), + make_bare_builtin_call("PRINT", 2, 1), + ], + ); + } + + #[test] + fn test_label_before_statement() { + do_ok_test( + "@foo PRINT", + &[ + Statement::Label(LabelSpan { name: "foo".to_owned(), name_pos: lc(1, 1) }), + make_bare_builtin_call("PRINT", 1, 6), + ], + ); + } + + #[test] + fn test_label_multiple_same_line() { + do_ok_test( + "@foo @bar", + &[ + Statement::Label(LabelSpan { name: "foo".to_owned(), name_pos: lc(1, 1) }), + Statement::Label(LabelSpan { name: "bar".to_owned(), name_pos: lc(1, 6) }), + ], + ); + } + + #[test] + fn test_label_errors() { + do_error_test("PRINT @foo", "1:7: Unexpected keyword in expression"); + } + + #[test] + fn test_parse_on_error_ok() { + do_ok_test("ON ERROR GOTO 0", &[Statement::OnError(OnErrorSpan::Reset)]); + + do_ok_test( + "ON ERROR GOTO 10", + &[Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target: "10".to_owned(), + target_pos: lc(1, 15), + }))], + ); + + do_ok_test( + "ON ERROR GOTO @foo", + &[Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target: "foo".to_owned(), + target_pos: lc(1, 15), + }))], + ); + + do_ok_test("ON ERROR RESUME NEXT", &[Statement::OnError(OnErrorSpan::ResumeNext)]); + } + + #[test] + fn test_parse_on_error_errors() { + do_error_test("ON", "1:3: Expected ERROR after ON"); + do_error_test("ON NEXT", "1:4: Expected ERROR after ON"); + do_error_test("ON ERROR", "1:9: Expected GOTO or RESUME after ON ERROR"); + do_error_test("ON ERROR FOR", "1:10: Expected GOTO or RESUME after ON ERROR"); + + do_error_test("ON ERROR RESUME", "1:16: Expected NEXT after ON ERROR RESUME"); + do_error_test("ON ERROR RESUME 3", "1:17: Expected NEXT after ON ERROR RESUME"); + do_error_test("ON ERROR RESUME NEXT 3", "1:22: Expected newline but found 3"); + + do_error_test("ON ERROR GOTO", "1:14: Expected label name or 0 after ON ERROR GOTO"); + do_error_test("ON ERROR GOTO NEXT", "1:15: Expected label name or 0 after ON ERROR GOTO"); + do_error_test("ON ERROR GOTO 0 @a", "1:17: Expected newline but found @a"); + } + + #[test] + fn test_select_empty() { + do_ok_test( + "SELECT CASE 7\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![], + end_pos: lc(2, 1), + })], + ); + + do_ok_test( + "SELECT CASE 5 - TRUE\n \nEND SELECT", + &[Statement::Select(SelectSpan { + expr: Expr::Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(5, 1, 13), + rhs: expr_boolean(true, 1, 17), + pos: lc(1, 15), + })), + cases: vec![], + end_pos: lc(3, 1), + })], + ); + } + + #[test] + fn test_select_case_else_only() { + do_ok_test( + "SELECT CASE 7\nCASE ELSE\nA\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![], + body: vec![make_bare_builtin_call("A", 3, 1)], + }], + end_pos: lc(4, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_without_else() { + do_ok_test( + "SELECT CASE 7\nCASE 1\nA\nCASE 2\nB\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 2, 6))], + body: vec![make_bare_builtin_call("A", 3, 1)], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 4, 6))], + body: vec![make_bare_builtin_call("B", 5, 1)], + }, + ], + end_pos: lc(6, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_with_else() { + do_ok_test( + "SELECT CASE 7\nCASE 1\nA\nCASE 2\nB\nCASE ELSE\nC\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 2, 6))], + body: vec![make_bare_builtin_call("A", 3, 1)], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 4, 6))], + body: vec![make_bare_builtin_call("B", 5, 1)], + }, + CaseSpan { guards: vec![], body: vec![make_bare_builtin_call("C", 7, 1)] }, + ], + end_pos: lc(8, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_empty_bodies() { + do_ok_test( + "SELECT CASE 7\nCASE 1\n\nCASE 2\n\nCASE ELSE\n\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 2, 6))], + body: vec![], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 4, 6))], + body: vec![], + }, + CaseSpan { guards: vec![], body: vec![] }, + ], + end_pos: lc(8, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_with_interleaved_end() { + let code = r#" + SELECT CASE 7 + CASE 1 + A + END + B + CASE 2 ' Second case. + C + END 8 + D + CASE ELSE + E + END + F + END SELECT + "#; + do_ok_test( + code, + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 2, 25), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 3, 22))], + body: vec![ + make_bare_builtin_call("A", 4, 21), + Statement::End(EndSpan { code: None }), + make_bare_builtin_call("B", 6, 21), + ], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 7, 22))], + body: vec![ + make_bare_builtin_call("C", 8, 21), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(9, 25) })), + }), + make_bare_builtin_call("D", 10, 21), + ], + }, + CaseSpan { + guards: vec![], + body: vec![ + make_bare_builtin_call("E", 12, 21), + Statement::End(EndSpan { code: None }), + make_bare_builtin_call("F", 14, 21), + ], + }, + ], + end_pos: lc(15, 13), + })], + ); + } + + #[test] + fn test_select_case_guards_equals() { + do_ok_test( + "SELECT CASE 7: CASE 9, 10, FALSE: END SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![ + CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(9, 1, 21)), + CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(10, 1, 24)), + CaseGuardSpan::Is(CaseRelOp::Equal, expr_boolean(false, 1, 28)), + ], + body: vec![], + }], + end_pos: lc(1, 35), + })], + ); + } + + #[test] + fn test_select_case_guards_is() { + do_ok_test( + "SELECT CASE 7: CASE IS = 1, IS <> 2, IS < 3, IS <= 4, IS > 5, IS >= 6: END SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![ + CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 1, 26)), + CaseGuardSpan::Is(CaseRelOp::NotEqual, expr_integer(2, 1, 35)), + CaseGuardSpan::Is(CaseRelOp::Less, expr_integer(3, 1, 43)), + CaseGuardSpan::Is(CaseRelOp::LessEqual, expr_integer(4, 1, 52)), + CaseGuardSpan::Is(CaseRelOp::Greater, expr_integer(5, 1, 60)), + CaseGuardSpan::Is(CaseRelOp::GreaterEqual, expr_integer(6, 1, 69)), + ], + body: vec![], + }], + end_pos: lc(1, 72), + })], + ); + } + + #[test] + fn test_select_case_guards_to() { + do_ok_test( + "SELECT CASE 7: CASE 1 TO 20, 10 TO 1: END SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![ + CaseGuardSpan::To(expr_integer(1, 1, 21), expr_integer(20, 1, 26)), + CaseGuardSpan::To(expr_integer(10, 1, 30), expr_integer(1, 1, 36)), + ], + body: vec![], + }], + end_pos: lc(1, 39), + })], + ); + } + + #[test] + fn test_select_errors() { + do_error_test("SELECT\n", "1:7: Expecting CASE after SELECT"); + do_error_test("SELECT CASE\n", "1:12: No expression in SELECT CASE statement"); + do_error_test("SELECT CASE 3 + 7", "1:18: Expecting newline after SELECT CASE"); + do_error_test("SELECT CASE 3 + 7 ,", "1:19: Expecting newline after SELECT CASE"); + do_error_test("SELECT CASE 3 + 7 IF", "1:19: Unexpected keyword in expression"); + + do_error_test("SELECT CASE 1\n", "1:1: SELECT without END SELECT"); + + do_error_test( + "SELECT CASE 1\nEND", + "2:1: Expected CASE after SELECT CASE before any statement", + ); + do_error_test( + "SELECT CASE 1\nEND IF", + "2:1: Expected CASE after SELECT CASE before any statement", + ); + do_error_test( + "SELECT CASE 1\na = 1", + "2:1: Expected CASE after SELECT CASE before any statement", + ); + + do_error_test( + "SELECT CASE 1\nCASE 1", + "2:7: Expected comma, newline, or TO after expression", + ); + do_error_test("SELECT CASE 1\nCASE ELSE", "2:10: Expecting newline after CASE"); + + do_error_test("SELECT CASE 1\nCASE ELSE\nEND", "1:1: SELECT without END SELECT"); + do_error_test("SELECT CASE 1\nCASE ELSE\nEND IF", "3:1: END IF without IF"); + + do_error_test("SELECT CASE 1\nCASE ELSE\nCASE ELSE\n", "3:1: CASE ELSE must be unique"); + do_error_test("SELECT CASE 1\nCASE ELSE\nCASE 1\n", "3:1: CASE ELSE is not last"); + } + + #[test] + fn test_select_case_errors() { + fn do_case_error_test(cases: &str, exp_error: &str) { + do_error_test(&format!("SELECT CASE 1\nCASE {}\n", cases), exp_error); + } + + do_case_error_test("ELSE, ELSE", "2:10: Expected newline after CASE ELSE"); + do_case_error_test("ELSE, 7", "2:10: Expected newline after CASE ELSE"); + do_case_error_test("7, ELSE", "2:9: CASE ELSE must be on its own"); + + do_case_error_test("IS 7", "2:9: Expected relational operator"); + do_case_error_test("IS AND", "2:9: Expected relational operator"); + do_case_error_test("IS END", "2:9: Expected relational operator"); + + do_case_error_test("IS <>", "2:11: Missing expression after relational operator"); + do_case_error_test("IS <> IF", "2:12: Unexpected keyword in expression"); + + do_case_error_test("", "2:6: Missing expression in CASE guard"); + do_case_error_test("2 + 5 TO", "2:14: Missing expression after TO in CASE guard"); + do_case_error_test("2 + 5 TO AS", "2:15: Missing expression after TO in CASE guard"); + do_case_error_test( + "2 + 5 TO 8 AS", + "2:17: Expected comma, newline, or TO after expression", + ); + } + + #[test] + fn test_sub_empty() { + do_ok_test( + "SUB foo\nEND SUB", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 5), + params: vec![], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_sub_some_content() { + do_ok_test( + r#" + SUB foo + A + END + END 8 + B + END SUB + "#, + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(2, 21), + params: vec![], + body: vec![ + make_bare_builtin_call("A", 3, 21), + Statement::End(EndSpan { code: None }), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(5, 25) })), + }), + make_bare_builtin_call("B", 6, 21), + ], + end_pos: lc(7, 17), + })], + ); + } + + #[test] + fn test_sub_one_param() { + do_ok_test( + "SUB foo(x)\nEND SUB", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 5), + params: vec![VarRef::new("x", None)], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_sub_multiple_params() { + do_ok_test( + "SUB foo(x$, y, z AS BOOLEAN)\nEND SUB", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 5), + params: vec![ + VarRef::new("x", Some(ExprType::Text)), + VarRef::new("y", None), + VarRef::new("z", Some(ExprType::Boolean)), + ], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_sub_errors() { + do_error_test("SUB", "1:4: Expected a function name after SUB"); + do_error_test("SUB foo", "1:8: Expected newline after SUB name"); + do_error_test("SUB foo 3", "1:9: Expected newline after SUB name"); + do_error_test("SUB foo\nEND", "1:1: SUB without END SUB"); + do_error_test("SUB foo\nEND IF", "2:1: END IF without IF"); + do_error_test("SUB foo\nEND FUNCTION", "2:1: END FUNCTION without FUNCTION"); + do_error_test( + "SUB foo\nSUB bar\nEND SUB\nEND SUB", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test( + "SUB foo\nFUNCTION bar\nEND FUNCTION\nEND SUB", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test("SUB foo (", "1:10: Expected a parameter name"); + do_error_test("SUB foo ()", "1:10: Expected a parameter name"); + do_error_test("SUB foo (,)", "1:10: Expected a parameter name"); + do_error_test("SUB foo (a,)", "1:12: Expected a parameter name"); + do_error_test("SUB foo (,b)", "1:10: Expected a parameter name"); + do_error_test("SUB foo (a AS)", "1:14: Invalid type name ) in AS type definition"); + do_error_test("SUB foo (a INTEGER)", "1:12: Expected comma, AS, or end of parameters list"); + do_error_test("SUB foo (a? AS BOOLEAN)", "1:10: Type annotation not allowed in a?"); + do_error_test( + "SUB foo$", + "1:5: SUBs cannot return a value so type annotations are not allowed", + ); + do_error_test( + "SUB foo$\nEND SUB", + "1:5: SUBs cannot return a value so type annotations are not allowed", + ); + } + + #[test] + fn test_while_empty() { + do_ok_test( + "WHILE 2 + 3\nWEND", + &[Statement::While(WhileSpan { + expr: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 7), + rhs: expr_integer(3, 1, 11), + pos: lc(1, 9), + })), + body: vec![], + })], + ); + do_ok_test( + "WHILE 5\n\nREM foo\n\nWEND\n", + &[Statement::While(WhileSpan { expr: expr_integer(5, 1, 7), body: vec![] })], + ); + } + + #[test] + fn test_while_loops() { + do_ok_test( + "WHILE TRUE\nA\nB\nWEND", + &[Statement::While(WhileSpan { + expr: expr_boolean(true, 1, 7), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_while_nested() { + let code = r#" + WHILE TRUE + A + WHILE FALSE + B + WEND + C + WEND + "#; + do_ok_test( + code, + &[Statement::While(WhileSpan { + expr: expr_boolean(true, 2, 19), + body: vec![ + make_bare_builtin_call("A", 3, 17), + Statement::While(WhileSpan { + expr: expr_boolean(false, 4, 23), + body: vec![make_bare_builtin_call("B", 5, 21)], + }), + make_bare_builtin_call("C", 7, 17), + ], + })], + ); + } + + #[test] + fn test_while_errors() { + do_error_test("WHILE\n", "1:6: No expression in WHILE statement"); + do_error_test("WHILE TRUE", "1:11: Expecting newline after WHILE"); + do_error_test("\n\nWHILE TRUE\n", "3:1: WHILE without WEND"); + do_error_test("WHILE TRUE\nEND", "1:1: WHILE without WEND"); + do_error_test("WHILE TRUE\nEND\n", "1:1: WHILE without WEND"); + do_error_test("WHILE TRUE\nEND WHILE\n", "2:5: Unexpected keyword in expression"); + + do_error_test("WHILE ,\nWEND", "1:7: No expression in WHILE statement"); + do_error_test("WHILE ,\nEND", "1:7: No expression in WHILE statement"); + } +} diff --git a/core2/src/reader.rs b/core2/src/reader.rs new file mode 100644 index 00000000..4ba3934e --- /dev/null +++ b/core2/src/reader.rs @@ -0,0 +1,320 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Character-based reader for an input stream with position tracking. + +use std::char; +use std::fmt; +use std::io::{self, BufRead}; + +/// Tab length used to compute the current position within a line when encountering a tab character. +const TAB_LENGTH: usize = 8; + +/// Representation of a position within a stream. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct LineCol { + /// Line number. + pub line: usize, + + /// Column number. + pub col: usize, +} + +impl fmt::Display for LineCol { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.line, self.col) + } +} + +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct CharSpan { + /// Character in this span. + pub(crate) ch: char, + + /// Position where this character starts. + pub(crate) pos: LineCol, +} + +/// Possible types of buffered data in the reader. +enum Pending { + /// Initial state of the reader where no data has been buffered yet. + Unknown, + + /// Intermediate state where the reader holds a line of text, broken down by character, and an + /// index to the character to return on the next read. + Chars(Vec, usize), + + /// Terminal state of the reader due to an EOF condition. + Eof, + + /// Terminal state of the reader due to an error. If not `None`, this contains the original + /// error that caused the problem. Otherwise, that error was already consumed (and thus + /// reaching this case indicates a problem in the caller) so we return an invalid state. + Error(Option), +} + +/// Wraps `io::Read` to offer an iterator over characters. +pub struct CharReader<'a> { + /// The wrapper reader from which to reach characters. + reader: io::BufReader<&'a mut dyn io::Read>, + + /// Current state of any buffered data. + pending: Pending, + + /// If not none, contains the character read by `peek`, which will be consumed by the next call + /// to `read`. + peeked: Option>>, + + /// Line and column number of the next character to be read. + next_pos: LineCol, +} + +impl<'a> CharReader<'a> { + /// Constructs a new character reader from an `io::Read`. + pub fn from(reader: &'a mut dyn io::Read) -> Self { + Self { + reader: io::BufReader::new(reader), + pending: Pending::Unknown, + peeked: None, + next_pos: LineCol { line: 1, col: 1 }, + } + } + + /// Replenishes `pending` with the next line to process. + fn refill_and_next(&mut self) -> Option> { + self.pending = { + let mut line = String::new(); + match self.reader.read_line(&mut line) { + Ok(0) => Pending::Eof, + Ok(_) => Pending::Chars(line.chars().collect(), 0), + Err(e) => Pending::Error(Some(e)), + } + }; + self.next() + } + + /// Peeks into the next character without consuming it. + pub(crate) fn peek(&mut self) -> Option<&io::Result> { + if self.peeked.is_none() { + let next = self.next(); + self.peeked.replace(next); + } + self.peeked.as_ref().unwrap().as_ref() + } + + /// Gets the current position of the read, which is the position that the next character will + /// carry. + pub(crate) fn next_pos(&self) -> LineCol { + self.next_pos + } +} + +impl Iterator for CharReader<'_> { + type Item = io::Result; + + fn next(&mut self) -> Option { + if let Some(peeked) = self.peeked.take() { + return peeked; + } + + match &mut self.pending { + Pending::Unknown => self.refill_and_next(), + Pending::Eof => None, + Pending::Chars(chars, last) => { + if *last == chars.len() { + self.refill_and_next() + } else { + let ch = chars[*last]; + *last += 1; + + let pos = self.next_pos; + match ch { + '\n' => { + self.next_pos.line += 1; + self.next_pos.col = 1; + } + '\t' => { + self.next_pos.col = + (self.next_pos.col - 1 + TAB_LENGTH) / TAB_LENGTH * TAB_LENGTH + 1; + } + _ => { + self.next_pos.col += 1; + } + } + + Some(Ok(CharSpan { ch, pos })) + } + } + Pending::Error(e) => match e.take() { + Some(e) => Some(Err(e)), + None => Some(Err(io::Error::other("Invalid state; error already consumed"))), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Syntactic sugar to instantiate a `CharSpan` for testing. + fn cs(ch: char, line: usize, col: usize) -> CharSpan { + CharSpan { ch, pos: LineCol { line, col } } + } + + #[test] + fn test_empty() { + let mut input = b"".as_ref(); + let mut reader = CharReader::from(&mut input); + assert!(reader.next().is_none()); + } + + #[test] + fn test_multibyte_chars() { + let mut input = "Hi 훌리오".as_bytes(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('H', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('i', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs(' ', 1, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('훌', 1, 4), reader.next().unwrap().unwrap()); + assert_eq!(cs('리', 1, 5), reader.next().unwrap().unwrap()); + assert_eq!(cs('오', 1, 6), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_consecutive_newlines() { + let mut input = b"a\n\nbc\n".as_ref(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('a', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 2, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('b', 3, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('c', 3, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 3, 3), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_tabs() { + let mut input = "1\t9\n1234567\t8\n12345678\t9".as_bytes(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('1', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\t', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('9', 1, 9), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 10), reader.next().unwrap().unwrap()); + assert_eq!(cs('1', 2, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('2', 2, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('3', 2, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('4', 2, 4), reader.next().unwrap().unwrap()); + assert_eq!(cs('5', 2, 5), reader.next().unwrap().unwrap()); + assert_eq!(cs('6', 2, 6), reader.next().unwrap().unwrap()); + assert_eq!(cs('7', 2, 7), reader.next().unwrap().unwrap()); + assert_eq!(cs('\t', 2, 8), reader.next().unwrap().unwrap()); + assert_eq!(cs('8', 2, 9), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 2, 10), reader.next().unwrap().unwrap()); + assert_eq!(cs('1', 3, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('2', 3, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('3', 3, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('4', 3, 4), reader.next().unwrap().unwrap()); + assert_eq!(cs('5', 3, 5), reader.next().unwrap().unwrap()); + assert_eq!(cs('6', 3, 6), reader.next().unwrap().unwrap()); + assert_eq!(cs('7', 3, 7), reader.next().unwrap().unwrap()); + assert_eq!(cs('8', 3, 8), reader.next().unwrap().unwrap()); + assert_eq!(cs('\t', 3, 9), reader.next().unwrap().unwrap()); + assert_eq!(cs('9', 3, 17), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_crlf() { + let mut input = b"a\r\nb".as_ref(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('a', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\r', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('b', 2, 1), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_past_eof_returns_eof() { + let mut input = b"a".as_ref(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('a', 1, 1), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_next_pos() { + let mut input = "Hi".as_bytes(); + let mut reader = CharReader::from(&mut input); + assert_eq!(LineCol { line: 1, col: 1 }, reader.next_pos()); + assert_eq!(cs('H', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(LineCol { line: 1, col: 2 }, reader.next_pos()); + assert_eq!(cs('i', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(LineCol { line: 1, col: 3 }, reader.next_pos()); + assert!(reader.next().is_none()); + assert_eq!(LineCol { line: 1, col: 3 }, reader.next_pos()); + } + + /// A reader that generates an error only on the Nth read operation. + /// + /// All other reads return a line with a single character in them with the assumption that the + /// `CharReader` issues a single read per line. If that assumption changes, the tests here may + /// start failing. + struct FaultyReader { + current_read: usize, + fail_at_read: usize, + } + + impl FaultyReader { + /// Creates a new reader that will fail at the `fail_at_read`th operation. + fn new(fail_at_read: usize) -> Self { + let current_read = 0; + FaultyReader { current_read, fail_at_read } + } + } + + impl io::Read for FaultyReader { + #[allow(clippy::branches_sharing_code)] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.current_read == self.fail_at_read { + self.current_read += 1; + Err(io::Error::from(io::ErrorKind::InvalidInput)) + } else { + self.current_read += 1; + buf[0] = b'1'; + buf[1] = b'\n'; + Ok(2) + } + } + } + + #[test] + fn test_errors_prevent_further_reads() { + let mut reader = FaultyReader::new(2); + let mut reader = CharReader::from(&mut reader); + assert_eq!(cs('1', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('1', 2, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 2, 2), reader.next().unwrap().unwrap()); + assert_eq!(io::ErrorKind::InvalidInput, reader.next().unwrap().unwrap_err().kind()); + assert_eq!(io::ErrorKind::Other, reader.next().unwrap().unwrap_err().kind()); + assert_eq!(io::ErrorKind::Other, reader.next().unwrap().unwrap_err().kind()); + } +} diff --git a/core2/src/vm.rs b/core2/src/vm.rs new file mode 100644 index 00000000..a8559158 --- /dev/null +++ b/core2/src/vm.rs @@ -0,0 +1,171 @@ +// EndBASIC +// Copyright 2026 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Virtual machine for EndBASIC execution. + +use crate::bytecode::{self, INSTR_FORMATTERS, IRegister, MAX_GLOBAL_REGISTERS}; +use crate::callable::{Callable, Scope}; +use crate::compiler::{Image, SymbolKey}; +use std::collections::HashMap; +use std::rc::Rc; + +const INSTR_HANDLERS: &[fn(&mut Context, u32)] = &[ + do_nop, + do_enter, + do_leave, + do_upcall, + do_move, + do_load_integer, + do_load_integer_constant, + do_add_integer, +]; + +fn do_nop(context: &mut Context, op: u32) { + bytecode::parse_nop(op); + context.pc += 1; +} + +fn do_enter(context: &mut Context, op: u32) { + let nlocals = bytecode::parse_enter(op); + for _ in 0..nlocals { + context.regs.push(0); + } + context.pc += 1; +} + +fn do_leave(context: &mut Context, op: u32) { + bytecode::parse_leave(op); + // TODO + context.pc += 1; +} + +fn do_upcall(context: &mut Context, op: u32) { + let (index, nargs) = bytecode::parse_upcall(op); + context.upcall = Some((index, nargs)); + context.pc += 1; +} + +fn do_move(context: &mut Context, op: u32) { + let (dest, src) = bytecode::parse_move(op); + let value = context.get_register(src); + context.set_register(dest, value); + context.pc += 1; +} + +fn do_load_integer(context: &mut Context, op: u32) { + let (register, i) = bytecode::parse_load_integer(op); + context.set_register(register, i as u32); + context.pc += 1; +} + +fn do_load_integer_constant(context: &mut Context, op: u32) { + let (register, index) = bytecode::parse_load_integer_constant(op); + eprintln!("Load integer constant {} into r{}", index, register); + context.pc += 1; +} + +fn do_add_integer(context: &mut Context, op: u32) { + let (dest, src1, src2) = bytecode::parse_add_integer(op); + let lhs = context.get_register(src1) as i32; + let rhs = context.get_register(src2) as i32; + context.set_register(dest, (lhs + rhs) as u32); + context.pc += 1; +} + +struct Frame { + old_pc: usize, + old_fp: usize, +} + +pub struct Context { + pc: usize, + fp: usize, + upcall: Option<(usize, usize)>, + regs: Vec, + //call_stack: Vec, +} + +impl Default for Context { + fn default() -> Self { + Self { + pc: 0, + fp: 0, + upcall: None, + regs: vec![0; MAX_GLOBAL_REGISTERS as usize], + //call_stack: vec![], + } + } +} + +impl Context { + fn get_register(&self, reg: IRegister) -> u32 { + self.regs[reg.to_index(self.fp)] + } + + fn set_register(&mut self, reg: IRegister, value: u32) { + self.regs[reg.to_index(self.fp)] = value; + } + + pub fn new_scope<'a>(&'a self, nargs: usize) -> Scope<'a> { + Scope { regs: &self.regs[self.regs.len() - nargs..] } + } +} + +pub enum StopReason { + Eof, + Upcall(usize, usize), +} + +fn disasm(code: &[u32]) { + for instr in code { + let opcode = ((*instr) >> 24) as usize; + eprintln!("{}", INSTR_FORMATTERS[opcode](*instr)); + } +} + +#[derive(Default)] +pub struct Vm { + upcalls_by_name: HashMap>, + image: Option, +} + +impl Vm { + pub fn new(upcalls_by_name: HashMap>) -> Self { + Self { upcalls_by_name, image: None } + } + + pub fn load(&mut self, image: Image) -> Context { + disasm(&image.code); + self.image = Some(image); + Context::default() + } + + pub fn exec(&self, context: &mut Context) -> StopReason { + context.upcall = None; + + let image = self.image.as_ref().unwrap(); + while context.pc < image.code.len() && context.upcall.is_none() { + let op = image.code[context.pc]; + let opcode = (op >> 24) as usize; + INSTR_HANDLERS[opcode](context, op); + } + + if let Some((index, nargs)) = context.upcall { + StopReason::Upcall(index, nargs) + } else { + StopReason::Eof + } + } +} diff --git a/repl/Cargo.toml b/repl/Cargo.toml index d196520c..3af843d6 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - REPL" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-trait = "0.1" diff --git a/repl/src/editor.rs b/repl/src/editor.rs index 8f296cf0..8ddf7cba 100644 --- a/repl/src/editor.rs +++ b/repl/src/editor.rs @@ -1220,7 +1220,8 @@ mod tests { "this is a line of text with more than 40 characters\nshort\na\n\nanother line of text with more than 40 characters\n", "this is a line of text with more than 40 characters\nshort\na\n\nanother line of text with more than 40 characters\n", cb, - ob); + ob, + ); } #[test] @@ -1330,7 +1331,8 @@ mod tests { "this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n", "this is a line of text with more than 40 characters\n\na\nshort\nanother line of text with more than 40 characters\n", cb, - ob); + ob, + ); } #[test] diff --git a/repl/src/lib.rs b/repl/src/lib.rs index fb8632bc..3fee7a7a 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -15,16 +15,9 @@ //! Interactive interpreter for the EndBASIC language. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use endbasic_core::exec::{Machine, StopReason}; -use endbasic_std::console::{self, is_narrow, refill_and_print, Console}; -use endbasic_std::program::{continue_if_modified, Program, BREAK_MSG}; +use endbasic_std::console::{self, Console, is_narrow, refill_and_print}; +use endbasic_std::program::{BREAK_MSG, Program, continue_if_modified}; use endbasic_std::storage::Storage; use std::cell::RefCell; use std::io; diff --git a/rpi/Cargo.toml b/rpi/Cargo.toml index 18c4d6d5..36bba0bf 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - Raspberry Pi support" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] rppal = "0.17" diff --git a/rpi/src/lib.rs b/rpi/src/lib.rs index c56f56e7..966d530d 100644 --- a/rpi/src/lib.rs +++ b/rpi/src/lib.rs @@ -24,4 +24,4 @@ mod gpio; pub use gpio::RppalPins; mod spi; -pub use spi::{spi_bus_open, RppalSpiBus}; +pub use spi::{RppalSpiBus, spi_bus_open}; diff --git a/rustfmt.toml b/rustfmt.toml index 3c319ea6..adaee164 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,3 @@ -edition = "2018" +edition = "2024" newline_style = "Unix" use_small_heuristics = "Max" diff --git a/sdl/Cargo.toml b/sdl/Cargo.toml index beedd6bd..44dcd12e 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - SDL graphical console" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-channel = "2.2" diff --git a/sdl/src/console.rs b/sdl/src/console.rs index 4363e4b1..33e81374 100644 --- a/sdl/src/console.rs +++ b/sdl/src/console.rs @@ -20,7 +20,7 @@ use async_channel::Sender; use async_trait::async_trait; use endbasic_core::exec::Signal; use endbasic_std::console::{ - remove_control_chars, CharsXY, ClearType, Console, Key, PixelsXY, Resolution, SizeInPixels, + CharsXY, ClearType, Console, Key, PixelsXY, Resolution, SizeInPixels, remove_control_chars, }; use std::io; use std::path::PathBuf; @@ -160,7 +160,7 @@ impl Console for SdlConsole { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Cannot leave alternate screen; not entered", - )) + )); } }; @@ -261,9 +261,9 @@ impl Console for SdlConsole { mod testutils { use super::*; use async_channel::{Receiver, TryRecvError}; + use flate2::Compression; use flate2::read::GzDecoder; use flate2::write::GzEncoder; - use flate2::Compression; use futures_lite::future::block_on; use once_cell::sync::Lazy; use sdl2::event::Event; diff --git a/sdl/src/font.rs b/sdl/src/font.rs index 76c252ae..0e009bb9 100644 --- a/sdl/src/font.rs +++ b/sdl/src/font.rs @@ -66,20 +66,23 @@ impl<'a> MonospacedFont<'a> { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Font lacks a glyph for 'A'; is it valid?", - )) + )); } }; let width = match u16::try_from(metrics.advance) { Ok(0) => { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid font width 0")) + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid font width 0", + )); } Ok(width) => width, Err(e) => { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Invalid font width {}: {}", metrics.advance, e), - )) + )); } }; @@ -88,14 +91,14 @@ impl<'a> MonospacedFont<'a> { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Invalid font height 0", - )) + )); } Ok(height) => height, Err(e) => { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Invalid font height {}: {}", font.height(), e), - )) + )); } }; diff --git a/sdl/src/host.rs b/sdl/src/host.rs index 658efdd1..a2302eb4 100644 --- a/sdl/src/host.rs +++ b/sdl/src/host.rs @@ -18,14 +18,14 @@ //! All communication with this thread happens via channels to ensure all SDL operations are invoked //! from a single thread. -use crate::font::{font_error_to_io_error, MonospacedFont}; +use crate::font::{MonospacedFont, font_error_to_io_error}; use crate::string_error_to_io_error; use async_trait::async_trait; use endbasic_core::exec::Signal; use endbasic_std::console::drawing::{draw_circle, draw_circle_filled}; use endbasic_std::console::graphics::{ClampedInto, ClampedMul, InputOps, RasterInfo, RasterOps}; use endbasic_std::console::{ - CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, Resolution, SizeInPixels, RGB, + CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, RGB, Resolution, SizeInPixels, }; use sdl2::event::Event; use sdl2::keyboard::{Keycode, Mod}; diff --git a/sdl/src/lib.rs b/sdl/src/lib.rs index 0c40ef59..20405fbc 100644 --- a/sdl/src/lib.rs +++ b/sdl/src/lib.rs @@ -15,13 +15,6 @@ //! SDL2-based graphics terminal emulator. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use async_channel::Sender; use endbasic_core::exec::Signal; use endbasic_std::console::{Console, ConsoleSpec, Resolution}; diff --git a/st7735s/Cargo.toml b/st7735s/Cargo.toml index 9e8c7ae5..446b3f28 100644 --- a/st7735s/Cargo.toml +++ b/st7735s/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - st7735s console" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-channel = "2.2" diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index a16fc113..06b19645 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -27,11 +27,11 @@ use async_channel::{Receiver, TryRecvError}; use async_trait::async_trait; use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ - CharsXY, ClearType, Console, ConsoleSpec, GraphicsConsole, Key, ParseError, PixelsXY, - SizeInPixels, RGB, + CharsXY, ClearType, Console, ConsoleSpec, GraphicsConsole, Key, ParseError, PixelsXY, RGB, + SizeInPixels, }; use endbasic_std::gfx::lcd::fonts::Fonts; -use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel}; +use endbasic_std::gfx::lcd::{BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel, to_xy_size}; use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiMode}; use std::io; diff --git a/std/Cargo.toml b/std/Cargo.toml index e5f93bdf..b5f6d61c 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - standard library" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-channel = "2.2" diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index 48fb1d55..eb729a33 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -21,6 +21,7 @@ use crate::strings::{ format_boolean, format_double, format_integer, parse_boolean, parse_double, parse_integer, }; use async_trait::async_trait; +use endbasic_core::LineCol; use endbasic_core::ast::{ArgSep, ExprType, Value, VarRef}; use endbasic_core::compiler::{ ArgSepSyntax, OptionalValueSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax, @@ -28,7 +29,6 @@ use endbasic_core::compiler::{ }; use endbasic_core::exec::{Error, Machine, Result, Scope, ValueTag}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; -use endbasic_core::LineCol; use std::borrow::Cow; use std::cell::RefCell; use std::convert::TryFrom; diff --git a/std/src/console/drawing.rs b/std/src/console/drawing.rs index bf83beec..3b71c2e2 100644 --- a/std/src/console/drawing.rs +++ b/std/src/console/drawing.rs @@ -254,7 +254,7 @@ where mod testutils { use super::*; use crate::console::graphics::RasterInfo; - use crate::console::{SizeInPixels, RGB}; + use crate::console::{RGB, SizeInPixels}; /// Representation of captured raster operations. #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] diff --git a/std/src/console/format.rs b/std/src/console/format.rs index abe9ae78..971d4cf4 100644 --- a/std/src/console/format.rs +++ b/std/src/console/format.rs @@ -38,11 +38,7 @@ fn refill(paragraph: &str, width: usize) -> Vec { // better to respect the original spacing of the paragraph. let spaces = if line.ends_with('.') { let first = word.chars().next().expect("Words cannot be empty"); - if first == first.to_ascii_uppercase() { - 2 - } else { - 1 - } + if first == first.to_ascii_uppercase() { 2 } else { 1 } } else { 1 }; diff --git a/std/src/console/graphics.rs b/std/src/console/graphics.rs index b8addaec..610038e4 100644 --- a/std/src/console/graphics.rs +++ b/std/src/console/graphics.rs @@ -16,8 +16,8 @@ //! Support to implement graphical consoles. use super::{ - ansi_color_to_rgb, remove_control_chars, AnsiColor, CharsXY, ClearType, Console, Key, - LineBuffer, PixelsXY, SizeInPixels, RGB, + AnsiColor, CharsXY, ClearType, Console, Key, LineBuffer, PixelsXY, RGB, SizeInPixels, + ansi_color_to_rgb, remove_control_chars, }; use async_trait::async_trait; use std::convert::TryFrom; @@ -39,21 +39,13 @@ pub trait ClampedInto { impl ClampedInto for i16 { fn clamped_into(self) -> usize { - if self < 0 { - 0 - } else { - self as usize - } + if self < 0 { 0 } else { self as usize } } } impl ClampedInto for u16 { fn clamped_into(self) -> i16 { - if self > u16::try_from(i16::MAX).unwrap() { - i16::MAX - } else { - self as i16 - } + if self > u16::try_from(i16::MAX).unwrap() { i16::MAX } else { self as i16 } } } @@ -83,11 +75,7 @@ impl ClampedInto for i32 { impl ClampedInto for u32 { fn clamped_into(self) -> u16 { - if self > u32::from(u16::MAX) { - u16::MAX - } else { - self as u16 - } + if self > u32::from(u16::MAX) { u16::MAX } else { self as u16 } } } @@ -100,22 +88,14 @@ pub trait ClampedMul { impl ClampedMul for u16 { fn clamped_mul(self, rhs: u16) -> i16 { let product = u32::from(self) * u32::from(rhs); - if product > i16::MAX as u32 { - i16::MAX - } else { - product as i16 - } + if product > i16::MAX as u32 { i16::MAX } else { product as i16 } } } impl ClampedMul for u16 { fn clamped_mul(self, rhs: u16) -> u16 { let product = u32::from(self) * u32::from(rhs); - if product > u16::MAX as u32 { - u16::MAX - } else { - product as u16 - } + if product > u16::MAX as u32 { u16::MAX } else { product as u16 } } } @@ -225,7 +205,7 @@ pub trait RasterOps { /// Moves the rectangular region specified by `x1y1` and `size` to `x2y2`. The original region /// is erased with the current drawing color. fn move_pixels(&mut self, x1y1: PixelsXY, x2y2: PixelsXY, size: SizeInPixels) - -> io::Result<()>; + -> io::Result<()>; /// Writes `text` starting at `xy` with the current drawing color. fn write_text(&mut self, xy: PixelsXY, text: &str) -> io::Result<()>; @@ -358,11 +338,7 @@ where /// Renders any buffered changes to the backing surface. fn present_canvas(&mut self) -> io::Result<()> { - if self.sync_enabled { - self.raster_ops.present_canvas() - } else { - Ok(()) - } + if self.sync_enabled { self.raster_ops.present_canvas() } else { Ok(()) } } /// Draws the cursor at the current position and saves the previous contents of the screen so @@ -569,7 +545,7 @@ where return Err(io::Error::new( io::ErrorKind::InvalidInput, "Cannot leave alternate screen; not entered", - )) + )); } }; @@ -707,11 +683,7 @@ where } fn sync_now(&mut self) -> io::Result<()> { - if self.sync_enabled { - Ok(()) - } else { - self.raster_ops.present_canvas() - } + if self.sync_enabled { Ok(()) } else { self.raster_ops.present_canvas() } } fn set_sync(&mut self, enabled: bool) -> io::Result { diff --git a/std/src/console/mod.rs b/std/src/console/mod.rs index 48df5ece..a616278d 100644 --- a/std/src/console/mod.rs +++ b/std/src/console/mod.rs @@ -28,7 +28,7 @@ use std::str; mod cmds; pub(crate) use cmds::add_all; mod colors; -pub use colors::{ansi_color_to_rgb, AnsiColor, RGB}; +pub use colors::{AnsiColor, RGB, ansi_color_to_rgb}; pub mod drawing; mod format; pub(crate) use format::refill_and_page; diff --git a/std/src/console/pager.rs b/std/src/console/pager.rs index 001bd2c6..797744af 100644 --- a/std/src/console/pager.rs +++ b/std/src/console/pager.rs @@ -15,7 +15,7 @@ //! A simple paginator for commands that produce long outputs -use super::{is_narrow, CharsXY, Console, Key}; +use super::{CharsXY, Console, Key, is_narrow}; use std::io; /// Message to print on a narrow console when the screen is full. diff --git a/std/src/console/trivial.rs b/std/src/console/trivial.rs index a6837b83..dc3c7022 100644 --- a/std/src/console/trivial.rs +++ b/std/src/console/trivial.rs @@ -16,7 +16,7 @@ //! Trivial stdio-based console implementation for when we have nothing else. use crate::console::{ - get_env_var_as_u16, read_key_from_stdin, remove_control_chars, CharsXY, ClearType, Console, Key, + CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars, }; use async_trait::async_trait; use std::collections::VecDeque; @@ -41,11 +41,7 @@ pub struct TrivialConsole { impl TrivialConsole { /// Flushes the console, which has already been written to via `lock`, if syncing is enabled. fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> { - if self.sync_enabled { - lock.flush() - } else { - Ok(()) - } + if self.sync_enabled { lock.flush() } else { Ok(()) } } } @@ -132,11 +128,7 @@ impl Console for TrivialConsole { } fn sync_now(&mut self) -> io::Result<()> { - if self.sync_enabled { - Ok(()) - } else { - io::stdout().flush() - } + if self.sync_enabled { Ok(()) } else { io::stdout().flush() } } fn set_sync(&mut self, enabled: bool) -> io::Result { diff --git a/std/src/exec.rs b/std/src/exec.rs index 03228b2e..c20b2be6 100644 --- a/std/src/exec.rs +++ b/std/src/exec.rs @@ -16,11 +16,11 @@ //! Commands that manipulate the machine's state or the program's execution. use async_trait::async_trait; +use endbasic_core::LineCol; use endbasic_core::ast::ExprType; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; use endbasic_core::exec::{Error, Machine, Result, Scope}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; -use endbasic_core::LineCol; use futures_lite::future::{BoxedLocal, FutureExt}; use std::borrow::Cow; use std::rc::Rc; diff --git a/std/src/gfx/lcd/buffered/mod.rs b/std/src/gfx/lcd/buffered/mod.rs index 52de314c..5d05549e 100644 --- a/std/src/gfx/lcd/buffered/mod.rs +++ b/std/src/gfx/lcd/buffered/mod.rs @@ -17,9 +17,9 @@ use crate::console::drawing; use crate::console::graphics::{RasterInfo, RasterOps}; -use crate::console::{CharsXY, PixelsXY, SizeInPixels, RGB}; +use crate::console::{CharsXY, PixelsXY, RGB, SizeInPixels}; use crate::gfx::lcd::fonts::Font; -use crate::gfx::lcd::{to_xy_size, AsByteSlice, Lcd, LcdSize, LcdXY}; +use crate::gfx::lcd::{AsByteSlice, Lcd, LcdSize, LcdXY, to_xy_size}; use std::convert::TryFrom; use std::io; @@ -115,11 +115,7 @@ where None } else { let value = usize::try_from(value).expect("Positive value must fit"); - if value > max { - None - } else { - Some(value) - } + if value > max { None } else { Some(value) } } } @@ -138,11 +134,7 @@ where 0 } else { let value = usize::try_from(value).expect("Positive value must fit"); - if value > max { - max - } else { - value - } + if value > max { max } else { value } } } @@ -164,11 +156,7 @@ where None } else { let value = usize::try_from(value).expect("Positive value must fit"); - if value > max { - Some(max) - } else { - Some(value) - } + if value > max { Some(max) } else { Some(value) } } } @@ -399,11 +387,7 @@ where } fn present_canvas(&mut self) -> io::Result<()> { - if self.sync { - Ok(()) - } else { - self.force_present_canvas() - } + if self.sync { Ok(()) } else { self.force_present_canvas() } } fn read_pixels(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result { diff --git a/std/src/gfx/lcd/buffered/tests.rs b/std/src/gfx/lcd/buffered/tests.rs index 18899313..05b81852 100644 --- a/std/src/gfx/lcd/buffered/tests.rs +++ b/std/src/gfx/lcd/buffered/tests.rs @@ -19,7 +19,7 @@ use super::testutils::*; use super::*; use crate::console::graphics::RasterOps; use crate::console::{CharsXY, PixelsXY, SizeInPixels}; -use crate::gfx::lcd::fonts::{FONT_16X16, FONT_5X8}; +use crate::gfx::lcd::fonts::{FONT_5X8, FONT_16X16}; #[test] fn test_new_does_nothing() { diff --git a/std/src/gfx/lcd/buffered/testutils.rs b/std/src/gfx/lcd/buffered/testutils.rs index d3306aa1..e46d9ae7 100644 --- a/std/src/gfx/lcd/buffered/testutils.rs +++ b/std/src/gfx/lcd/buffered/testutils.rs @@ -162,11 +162,16 @@ impl Tester { // Print the difference as a bunch of expect_pixel lines that can be // copy-pasted into the code to re-define golden data. eprintln!( - ".expect_pixel(xy({:3}, {:3}), ({:3}, {:3}, {:3})) // got ({:3}, {:3}, {:3})", - x, y, - pixel[0], pixel[1], pixel[2], - exp_pixel[0], exp_pixel[1], exp_pixel[2], - ); + ".expect_pixel(xy({:3}, {:3}), ({:3}, {:3}, {:3})) // got ({:3}, {:3}, {:3})", + x, + y, + pixel[0], + pixel[1], + pixel[2], + exp_pixel[0], + exp_pixel[1], + exp_pixel[2], + ); } } } diff --git a/std/src/gfx/lcd/fonts/font_16x16.rs b/std/src/gfx/lcd/fonts/font_16x16.rs index 9d306fb4..142c22fd 100644 --- a/std/src/gfx/lcd/fonts/font_16x16.rs +++ b/std/src/gfx/lcd/fonts/font_16x16.rs @@ -22,8 +22,8 @@ //! Square 16x16 font. -use crate::gfx::lcd::fonts::Font; use crate::gfx::lcd::LcdSize; +use crate::gfx::lcd::fonts::Font; /// Width of the font glyphs in pixels. const WIDTH: usize = 16; diff --git a/std/src/gfx/lcd/fonts/font_5x8.rs b/std/src/gfx/lcd/fonts/font_5x8.rs index 7f32b664..88056cd4 100644 --- a/std/src/gfx/lcd/fonts/font_5x8.rs +++ b/std/src/gfx/lcd/fonts/font_5x8.rs @@ -52,8 +52,8 @@ //! Small font for tiny displays. -use crate::gfx::lcd::fonts::Font; use crate::gfx::lcd::LcdSize; +use crate::gfx::lcd::fonts::Font; /// Width of the font glyphs in pixels. const WIDTH: usize = 5; diff --git a/std/src/gfx/lcd/mod.rs b/std/src/gfx/lcd/mod.rs index 07e16148..90a093d2 100644 --- a/std/src/gfx/lcd/mod.rs +++ b/std/src/gfx/lcd/mod.rs @@ -15,7 +15,7 @@ //! Generic types to represent and manipulate LCDs. -use crate::console::{SizeInPixels, RGB}; +use crate::console::{RGB, SizeInPixels}; use std::convert::TryFrom; use std::io; diff --git a/std/src/gfx/mod.rs b/std/src/gfx/mod.rs index abf7ac00..6687e2c1 100644 --- a/std/src/gfx/mod.rs +++ b/std/src/gfx/mod.rs @@ -17,11 +17,11 @@ use crate::console::{Console, PixelsXY}; use async_trait::async_trait; +use endbasic_core::LineCol; use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; use endbasic_core::exec::{Error, Machine, Result, Scope}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; -use endbasic_core::LineCol; use std::borrow::Cow; use std::cell::RefCell; use std::convert::TryFrom; diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index 242efb23..e8fd5f96 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -16,11 +16,11 @@ //! GPIO access functions and commands for EndBASIC. use async_trait::async_trait; +use endbasic_core::LineCol; use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; use endbasic_core::exec::{Clearable, Error, Machine, Result, Scope}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder, Symbols}; -use endbasic_core::LineCol; use std::borrow::Cow; use std::cell::RefCell; use std::io; diff --git a/std/src/help.rs b/std/src/help.rs index 585eb7a1..d65d32f0 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -15,14 +15,14 @@ //! Interactive help support. -use crate::console::{refill_and_page, AnsiColor, Console, Pager}; +use crate::console::{AnsiColor, Console, Pager, refill_and_page}; use crate::exec::CATEGORY; use async_trait::async_trait; +use endbasic_core::LineCol; use endbasic_core::ast::ExprType; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; use endbasic_core::exec::{Error, Machine, Result, Scope}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder, Symbols}; -use endbasic_core::LineCol; use radix_trie::{Trie, TrieCommon}; use std::borrow::Cow; use std::cell::RefCell; @@ -293,11 +293,7 @@ fn parse_lang_reference(lang_md: &'static str) -> Vec<(&'static str, &'static st let section = §ion[title_end + body_start.len()..]; let end = section.find(section_start).unwrap_or_else(|| { - if section.ends_with(line_end) { - section.len() - line_end.len() - } else { - section.len() - } + if section.ends_with(line_end) { section.len() - line_end.len() } else { section.len() } }); let content = §ion[..end]; topics.push((title, content)); diff --git a/std/src/lib.rs b/std/src/lib.rs index ec0d1085..a1e4afb3 100644 --- a/std/src/lib.rs +++ b/std/src/lib.rs @@ -15,13 +15,6 @@ //! The EndBASIC standard library. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use async_channel::{Receiver, Sender}; use endbasic_core::exec::{Machine, Result, Signal, YieldNowFn}; use std::cell::RefCell; diff --git a/std/src/program.rs b/std/src/program.rs index 2b82a45d..2a249785 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -15,12 +15,12 @@ //! Stored program manipulation. -use crate::console::{read_line, Console, Pager}; +use crate::console::{Console, Pager, read_line}; use crate::storage::Storage; use crate::strings::parse_boolean; use async_trait::async_trait; use endbasic_core::ast::ExprType; -use endbasic_core::compiler::{compile, ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; +use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax, compile}; use endbasic_core::exec::{Machine, Result, Scope, StopReason}; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index 59808c4d..153de247 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -16,7 +16,7 @@ //! File system interaction. use super::time_format_error_to_io_error; -use crate::console::{is_narrow, Console, Pager}; +use crate::console::{Console, Pager, is_narrow}; use crate::storage::Storage; use async_trait::async_trait; use endbasic_core::ast::{ArgSep, ExprType}; diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index 73fe0ef3..9b64ee4e 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -230,7 +230,7 @@ impl Location { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Too many : separators in path '{}'", s), - )) + )); } }; @@ -466,7 +466,7 @@ impl Storage { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("Unknown mount scheme '{}'", scheme), - )) + )); } }; self.attach(name, uri, drive) diff --git a/std/src/strings.rs b/std/src/strings.rs index 573794d3..b2d91343 100644 --- a/std/src/strings.rs +++ b/std/src/strings.rs @@ -32,11 +32,7 @@ const CATEGORY: &str = "String and character functions"; /// Formats a boolean `b` for display. pub fn format_boolean(b: bool) -> &'static str { - if b { - "TRUE" - } else { - "FALSE" - } + if b { "TRUE" } else { "FALSE" } } /// Parses a string `s` as a boolean. @@ -53,11 +49,7 @@ pub fn parse_boolean(s: &str) -> std::result::Result { /// Formats a double `d` for display. pub fn format_double(d: f64) -> String { - if !d.is_nan() && d.is_sign_negative() { - d.to_string() - } else { - format!(" {}", d) - } + if !d.is_nan() && d.is_sign_negative() { d.to_string() } else { format!(" {}", d) } } /// Parses a string `s` as a double. @@ -70,11 +62,7 @@ pub fn parse_double(s: &str) -> std::result::Result { /// Formats an integer `i` for display. pub fn format_integer(i: i32) -> String { - if i.is_negative() { - i.to_string() - } else { - format!(" {}", i) - } + if i.is_negative() { i.to_string() } else { format!(" {}", i) } } /// Parses a string `s` as an integer. diff --git a/std/src/testutils.rs b/std/src/testutils.rs index 8fe7edf3..4856081d 100644 --- a/std/src/testutils.rs +++ b/std/src/testutils.rs @@ -16,7 +16,7 @@ //! Test utilities for consumers of the EndBASIC interpreter. use crate::console::{ - self, remove_control_chars, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels, + self, CharsXY, ClearType, Console, Key, PixelsXY, SizeInPixels, remove_control_chars, }; use crate::gpio; use crate::program::Program; diff --git a/std/tests/integration_test.rs b/std/tests/integration_test.rs index fe86d1b5..f14592a2 100644 --- a/std/tests/integration_test.rs +++ b/std/tests/integration_test.rs @@ -15,11 +15,6 @@ //! Integration tests that use golden input and output files. -// Keep these in sync with other top-level files. -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use std::env; use std::fs::File; use std::io::Read; @@ -76,11 +71,7 @@ fn read_golden(path: &Path) -> String { let mut golden = vec![]; f.read_to_end(&mut golden).expect("Failed to read golden data file"); let raw = String::from_utf8(golden).expect("Golden data file is not valid UTF-8"); - if cfg!(target_os = "windows") { - raw.replace("\r\n", "\n") - } else { - raw - } + if cfg!(target_os = "windows") { raw.replace("\r\n", "\n") } else { raw } } /// Runs `bin` with arguments `args` and checks its behavior against expectations. diff --git a/terminal/Cargo.toml b/terminal/Cargo.toml index 1a24c95e..0bbac725 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -9,7 +9,10 @@ description = "The EndBASIC programming language - terminal console" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" + +[lints] +workspace = true [dependencies] async-channel = "2.2" diff --git a/terminal/src/lib.rs b/terminal/src/lib.rs index 2bafa075..d3fbb61a 100644 --- a/terminal/src/lib.rs +++ b/terminal/src/lib.rs @@ -15,22 +15,15 @@ //! Crossterm-based console for terminal interaction. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use async_channel::{Receiver, Sender, TryRecvError}; use async_trait::async_trait; use crossterm::event::{self, KeyEventKind}; use crossterm::tty::IsTty; -use crossterm::{cursor, style, terminal, QueueableCommand}; +use crossterm::{QueueableCommand, cursor, style, terminal}; use endbasic_core::exec::Signal; use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ - get_env_var_as_u16, read_key_from_stdin, remove_control_chars, CharsXY, ClearType, Console, Key, + CharsXY, ClearType, Console, Key, get_env_var_as_u16, read_key_from_stdin, remove_control_chars, }; use std::cmp::Ordering; use std::collections::VecDeque; @@ -227,11 +220,7 @@ impl TerminalConsole { /// Flushes the console, which has already been written to via `lock`, if syncing is enabled. fn maybe_flush(&self, mut lock: StdoutLock<'_>) -> io::Result<()> { - if self.sync_enabled { - lock.flush() - } else { - Ok(()) - } + if self.sync_enabled { lock.flush() } else { Ok(()) } } } @@ -431,11 +420,7 @@ impl Console for TerminalConsole { } fn sync_now(&mut self) -> io::Result<()> { - if self.sync_enabled { - Ok(()) - } else { - io::stdout().flush() - } + if self.sync_enabled { Ok(()) } else { io::stdout().flush() } } fn set_sync(&mut self, enabled: bool) -> io::Result { diff --git a/web/Cargo.toml b/web/Cargo.toml index 36d30fb3..83552cc5 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -9,10 +9,13 @@ description = "The EndBASIC programming language - web interface" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" build = "build.rs" publish = false +[lints] +workspace = true + [lib] crate-type = ["cdylib", "rlib"] diff --git a/web/build.rs b/web/build.rs index b17397df..8d852af7 100644 --- a/web/build.rs +++ b/web/build.rs @@ -1,3 +1,20 @@ +// EndBASIC +// Copyright 2020 Julio Merino +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +//! Build script for the crate. + use vergen::EmitBuilder; fn main() { diff --git a/web/src/canvas.rs b/web/src/canvas.rs index 12d16c30..f39b561c 100644 --- a/web/src/canvas.rs +++ b/web/src/canvas.rs @@ -15,17 +15,17 @@ //! HTML canvas-based console implementation. -use crate::{log_and_panic, Yielder}; +use crate::{Yielder, log_and_panic}; use async_trait::async_trait; use endbasic_std::console::graphics::{RasterInfo, RasterOps}; -use endbasic_std::console::{CharsXY, PixelsXY, SizeInPixels, RGB}; +use endbasic_std::console::{CharsXY, PixelsXY, RGB, SizeInPixels}; use std::cell::RefCell; use std::convert::TryFrom; use std::f64::consts::PI; use std::io; use std::rc::Rc; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; use web_sys::HtmlCanvasElement; use web_sys::ImageData; use web_sys::{CanvasRenderingContext2d, ContextAttributes2d}; @@ -67,7 +67,7 @@ fn html_canvas_to_2d_context(canvas: HtmlCanvasElement) -> io::Result io::Result Key { Some(data) => data.chars().collect::>(), None => vec![], }; - if chars.len() == 1 { - Key::Char(chars[0]) - } else { - Key::Unknown - } + if chars.len() == 1 { Key::Char(chars[0]) } else { Key::Unknown } } /// Converts an HTML keyboard event into our own `Key` representation. @@ -68,11 +64,7 @@ fn on_key_event_into_key(dom_event: KeyboardEvent) -> Key { _ => { let printable = !dom_event.alt_key() && !dom_event.ctrl_key() && !dom_event.meta_key(); let chars = dom_event.key().chars().collect::>(); - if printable && chars.len() == 1 { - Key::Char(chars[0]) - } else { - Key::Unknown - } + if printable && chars.len() == 1 { Key::Char(chars[0]) } else { Key::Unknown } } } } @@ -104,11 +96,10 @@ impl OnScreenKeyboard { /// Pushes a new captured `dom_event` keyboard event into the input. pub fn inject_keyboard_event(&self, dom_event: KeyboardEvent) { let key = on_key_event_into_key(dom_event); - if key == Key::Interrupt { - if let Err(e) = self.signals_tx.try_send(Signal::Break) { + if key == Key::Interrupt + && let Err(e) = self.signals_tx.try_send(Signal::Break) { log_and_panic!("Send to unbounded channel must succeed: {}", e); } - } self.safe_try_send(key) } diff --git a/web/src/lib.rs b/web/src/lib.rs index d792201c..196d1db7 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -15,16 +15,9 @@ //! Web interface for the EndBASIC language. -// Keep these in sync with other top-level files. -#![allow(clippy::await_holding_refcell_ref)] -#![allow(clippy::collapsible_else_if)] -#![warn(anonymous_parameters, bad_style, missing_docs)] -#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)] -#![warn(unsafe_code)] - use async_channel::{Receiver, Sender}; -use endbasic_core::exec::{Error, Result, Signal, YieldNowFn}; use endbasic_core::LineCol; +use endbasic_core::exec::{Error, Result, Signal, YieldNowFn}; use endbasic_std::console::{Console, GraphicsConsole}; use std::cell::RefCell; use std::future::Future; @@ -34,8 +27,8 @@ use std::rc::Rc; use std::time::Duration; use time::OffsetDateTime; use url::Url; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; #[cfg(test)] use wasm_bindgen_test::wasm_bindgen_test_configure; use web_sys::HtmlCanvasElement; @@ -281,10 +274,9 @@ impl WebTerminal { let mut machine = match builder.build() { Ok(machine) => machine, Err(e) => { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Machine initialization failed: {}", e), - )) + )); } }; diff --git a/web/src/store.rs b/web/src/store.rs index a2a50c43..e8478f03 100644 --- a/web/src/store.rs +++ b/web/src/store.rs @@ -158,10 +158,9 @@ impl WebDrive { Ok(Some(key)) => key, Ok(None) => return Err(io::Error::other("Entry vanished")), Err(e) => { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Failed to fetch local storage entry with index {}: {:?}", i, e), - )) + )); } }; @@ -184,23 +183,20 @@ impl WebDrive { Ok(Some(content)) => content, Ok(None) => return Err(io::Error::new(io::ErrorKind::NotFound, "File not found")), Err(e) => { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Failed to get local storage entry with key {}: {:?}", old, e), - )) + )); } }; if let Err(e) = self.storage.set(new, &raw) { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Failed to put local storage entry with key {}: {:?}", new, e), )); }; if let Err(e) = self.storage.delete(old) { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Failed to put remove storage entry with key {}: {:?}", old, e), )); }; @@ -215,10 +211,9 @@ impl WebDrive { Ok(Some(content)) => content, Ok(None) => return Err(io::Error::new(io::ErrorKind::NotFound, "File not found")), Err(e) => { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Failed to get local storage entry with key {}: {:?}", key, e), - )) + )); } }; @@ -246,8 +241,7 @@ impl Drive for WebDrive { match self.storage.delete(key) { Ok(()) => Ok(()), - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, + Err(e) => Err(io::Error::other( format!("Failed to put remove storage entry with key {}: {:?}", key, e), )), } @@ -265,10 +259,9 @@ impl Drive for WebDrive { Ok(Some(key)) => key, Ok(None) => return Err(io::Error::other("Entry vanished")), Err(e) => { - return Err(io::Error::new( - io::ErrorKind::Other, + return Err(io::Error::other( format!("Failed to fetch local storage entry with index {}: {:?}", i, e), - )) + )); } }; @@ -296,8 +289,7 @@ impl Drive for WebDrive { let key = key.serialized(); match self.storage.set(key, &serde_json::to_string(&entry)?) { Ok(()) => Ok(()), - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, + Err(e) => Err(io::Error::other( format!("Failed to put local storage entry with key {}: {:?}", key, e), )), }