From d63c41767df9cfaf79bd740c9cdc529cf9f819b5 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 12 Oct 2024 06:45:48 -0700 Subject: [PATCH 001/110] Post-release version bump to 0.11.99 --- NEWS.md | 6 ++++++ cli/Cargo.toml | 16 ++++++++-------- client/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- repl/Cargo.toml | 6 +++--- rpi/Cargo.toml | 8 ++++---- sdl/Cargo.toml | 6 +++--- std/Cargo.toml | 4 ++-- terminal/Cargo.toml | 6 +++--- web/Cargo.toml | 10 +++++----- web/package-lock.json | 4 ++-- web/package.json | 2 +- 12 files changed, 41 insertions(+), 35 deletions(-) diff --git a/NEWS.md b/NEWS.md index b67a3c5a..1699a5ff 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,12 @@ to talk to the cloud service. If you use the web interface, this should not be a problem, but if you use local builds, please try to stay on the latest release for the time being.** +## Changes in version 0.11.99 + +STILL UNDER DEVELOPMENT; NOT RELEASED YET. + +* No changes recorded. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4e7e2bb7..e2cdc022 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -25,33 +25,33 @@ getopts = "0.2" thiserror = "1.0" [dependencies.endbasic-client] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../client" [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-repl] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../repl" [dependencies.endbasic-rpi] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../rpi" optional = true [dependencies.endbasic-sdl] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../sdl" optional = true [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.endbasic-terminal] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../terminal" optional = true diff --git a/client/Cargo.toml b/client/Cargo.toml index 59c63bba..65d0063d 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-client" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -21,11 +21,11 @@ time = { version = "0.3", features = ["std"] } url = "2.2" [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.reqwest] diff --git a/core/Cargo.toml b/core/Cargo.toml index dbf1a763..28ec89ad 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-core" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] diff --git a/repl/Cargo.toml b/repl/Cargo.toml index 249cf586..d196520c 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-repl" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -16,11 +16,11 @@ async-trait = "0.1" time = { version = "0.3", features = ["std"] } [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dev-dependencies] diff --git a/rpi/Cargo.toml b/rpi/Cargo.toml index 9bd05e72..6c70a6d1 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-rpi" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -18,15 +18,15 @@ rppal = "0.17" tokio = { version = "1", features = ["full"] } [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.endbasic-terminal] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../terminal" [dev-dependencies] diff --git a/sdl/Cargo.toml b/sdl/Cargo.toml index dde9c7ee..07f2b981 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-sdl" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -18,11 +18,11 @@ once_cell = "1.8" tempfile = "3" [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.sdl2] diff --git a/std/Cargo.toml b/std/Cargo.toml index f1d1fd12..b82dc2ee 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-std" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -19,7 +19,7 @@ radix_trie = "0.2" time = { version = "0.3", features = ["formatting", "local-offset", "std"] } [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" # We don't directly use getrandom but rand does, and we have to customize how diff --git a/terminal/Cargo.toml b/terminal/Cargo.toml index 3e8986e0..1a24c95e 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-terminal" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -18,10 +18,10 @@ crossterm = "0.27" tokio = { version = "1", features = ["rt"] } [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" diff --git a/web/Cargo.toml b/web/Cargo.toml index 1d13d8cb..3a13e7e3 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-web" -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -32,19 +32,19 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" [dependencies.endbasic-client] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../client" [dependencies.endbasic-core] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-repl] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../repl" [dependencies.endbasic-std] -version = "0.11.1" # ENDBASIC-VERSION +version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.web-sys] diff --git a/web/package-lock.json b/web/package-lock.json index f4d7f76a..c0298f28 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "endbasic-www", - "version": "0.11.1", + "version": "0.11.99", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "endbasic-www", - "version": "0.11.1", + "version": "0.11.99", "license": "Apache-2.0", "dependencies": { "jquery": "3.7.1" diff --git a/web/package.json b/web/package.json index ebfd5dce..cedf4f9e 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "endbasic-www", - "version": "0.11.1", + "version": "0.11.99", "description": "The EndBASIC programming language - web interface", "private": true, "scripts": { From 234449c71476768d3a8ec67496d760523dc1bad9 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 12 Oct 2024 06:46:20 -0700 Subject: [PATCH 002/110] Implement Display for LineCol and use it --- core/src/compiler/args.rs | 4 ++-- core/src/compiler/mod.rs | 21 ++++++++------------- core/src/exec.rs | 25 ++++++++++--------------- core/src/parser.rs | 2 +- core/src/reader.rs | 7 +++++++ std/src/program.rs | 2 +- 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index f2d64486..0618166a 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -829,8 +829,8 @@ mod testutils { /// Formats a `CallError` as a string to simplify comparisons. fn format_call_error(e: CallError) -> String { match e { - CallError::ArgumentError(pos, e) => format!("{}:{}: {}", pos.line, pos.col, e), - CallError::EvalError(pos, e) => format!("{}:{}: {}", pos.line, pos.col, e), + CallError::ArgumentError(pos, e) => format!("{}: {}", pos, e), + CallError::EvalError(pos, e) => format!("{}: {}", pos, e), CallError::InternalError(_pos, e) => panic!("Must not happen here: {}", e), CallError::IoError(e) => panic!("Must not happen here: {}", e), CallError::NestedError(e) => panic!("Must not happen here: {}", e), diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 0f241705..0c13525c 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -33,7 +33,7 @@ use exprs::{compile_expr, compile_expr_as_type, compile_expr_in_command}; /// Compilation errors. #[derive(Debug, thiserror::Error)] -#[error("{}:{}: {}", pos.line, pos.col, message)] +#[error("{}: {}", pos, message)] pub struct Error { pub(crate) pos: LineCol, pub(crate) message: String, @@ -52,24 +52,19 @@ impl Error { // that `CallError` can specify errors that make no sense in this context. fn from_call_error(md: &CallableMetadata, e: CallError, pos: LineCol) -> Self { match e { - CallError::ArgumentError(pos2, e) => Self::new( - pos, - format!("In call to {}: {}:{}: {}", md.name(), pos2.line, pos2.col, e), - ), + CallError::ArgumentError(pos2, e) => { + Self::new(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) + } CallError::EvalError(pos2, e) => { if !md.is_function() { Self::new(pos2, e) } else { - Self::new( - pos, - format!("In call to {}: {}:{}: {}", md.name(), pos2.line, pos2.col, e), - ) + Self::new(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) } } - CallError::InternalError(pos2, e) => Self::new( - pos, - format!("In call to {}: {}:{}: {}", md.name(), pos2.line, pos2.col, e), - ), + CallError::InternalError(pos2, e) => { + Self::new(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) + } CallError::IoError(_) => unreachable!("I/O errors are not possible during compilation"), CallError::NestedError(_) => unreachable!("Nested are not possible during compilation"), CallError::SyntaxError if md.syntax().is_empty() => { diff --git a/core/src/exec.rs b/core/src/exec.rs index 4d3f7f8e..dedc076c 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -39,7 +39,7 @@ pub enum Error { CompilerError(#[from] compiler::Error), /// Evaluation error during execution. - #[error("{}:{}: {}", .0.line, .0.col, .1)] + #[error("{}: {}", .0, .1)] EvalError(LineCol, String), /// I/O error during execution. @@ -56,7 +56,7 @@ pub enum Error { ParseError(#[from] parser::Error), /// Syntax error. - #[error("{}:{}: {}", .0.line, .0.col, .1)] + #[error("{}: {}", .0, .1)] SyntaxError(LineCol, String), /// Value computation error during execution. @@ -71,27 +71,22 @@ impl Error { // somehow unified with the equivalent function in eval::Error. fn from_call_error(md: &CallableMetadata, e: CallError, pos: LineCol) -> Self { match e { - CallError::ArgumentError(pos2, e) => Self::SyntaxError( - pos, - format!("In call to {}: {}:{}: {}", md.name(), pos2.line, pos2.col, e), - ), + CallError::ArgumentError(pos2, e) => { + Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) + } CallError::EvalError(pos2, e) => { if !md.is_function() { Self::EvalError(pos2, e) } else { - Self::EvalError( - pos, - format!("In call to {}: {}:{}: {}", md.name(), pos2.line, pos2.col, e), - ) + Self::EvalError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) } } - CallError::InternalError(pos2, e) => Self::SyntaxError( - pos, - format!("In call to {}: {}:{}: {}", md.name(), pos2.line, pos2.col, e), - ), + CallError::InternalError(pos2, e) => { + Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) + } CallError::IoError(e) => Self::IoError(io::Error::new( e.kind(), - format!("{}:{}: In call to {}: {}", pos.line, pos.col, md.name(), e), + format!("{}: In call to {}: {}", pos, md.name(), e), )), CallError::NestedError(e) => Self::NestedError(e), CallError::SyntaxError if md.syntax().is_empty() => { diff --git a/core/src/parser.rs b/core/src/parser.rs index 01e164bd..a94304b5 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -25,7 +25,7 @@ use std::io; #[derive(Debug, thiserror::Error)] pub enum Error { /// Bad syntax in the input program. - #[error("{}:{}: {}", .0.line, .0.col, .1)] + #[error("{}: {}", .0, .1)] Bad(LineCol, String), /// I/O error while parsing the input program. diff --git a/core/src/reader.rs b/core/src/reader.rs index 18334253..47908725 100644 --- a/core/src/reader.rs +++ b/core/src/reader.rs @@ -17,6 +17,7 @@ use std::cell::RefCell; use std::char; +use std::fmt; use std::io::{self, BufRead}; use std::rc::Rc; @@ -33,6 +34,12 @@ pub struct LineCol { 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 { diff --git a/std/src/program.rs b/std/src/program.rs index 0ccea282..38940ff2 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -212,7 +212,7 @@ impl Callable for DisasmCommand { while line.len() < 44 { line.push(' '); } - line += &format!(" # {}:{}", pos.line, pos.col); + line += &format!(" # {}", pos); } pager.print(&line).await?; } From 011cf85a446e92f502ba5dc51c4d2e9bbc9243e8 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 12 Oct 2024 08:23:15 -0700 Subject: [PATCH 003/110] Remove Machine::get_var_as* These helper methods were added to showcase how one could trivially write a configuration parser using EndBASIC... but aren't useful for anything else and their existence adds some complications in error reporting. So remove them for simplicity. --- core/examples/config.rs | 30 ++++++---- core/src/exec.rs | 126 +++++----------------------------------- core/src/syms.rs | 1 + core/tests/config.out | 2 +- 4 files changed, 36 insertions(+), 123 deletions(-) diff --git a/core/examples/config.rs b/core/examples/config.rs index 9e60e0d8..341baa4a 100644 --- a/core/examples/config.rs +++ b/core/examples/config.rs @@ -20,7 +20,11 @@ //! the scripted code cannot call back into Rust land, so the script's execution is guaranteed to //! not have side-effects. -use endbasic_core::exec::{Machine, StopReason}; +use endbasic_core::{ + ast::Value, + exec::{Machine, StopReason}, + syms::Symbol, +}; use futures_lite::future::block_on; /// Sample configuration file to parse. @@ -45,16 +49,22 @@ fn main() { } // Now that our script has run, inspect the variables it set on the machine. - match machine.get_var_as_int("foo_value") { - Ok(i) => println!("foo_value is {}", i), - Err(e) => println!("Input did not contain foo_value: {}", e), + 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_var_as_bool("enable_bar") { - Ok(b) => println!("enable_bar is {}", b), - Err(e) => println!("Input did not contain enable_bar: {}", e), + 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_var_as_string("enable_baz") { - Ok(b) => println!("enable_bar is {}", b), - Err(e) => println!("enable_baz is not set: {}", e), + 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/core/src/exec.rs b/core/src/exec.rs index dedc076c..3b463048 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -58,10 +58,6 @@ pub enum Error { /// Syntax error. #[error("{}: {}", .0, .1)] SyntaxError(LineCol, String), - - /// Value computation error during execution. - #[error("{0}")] - ValueError(value::Error), } impl Error { @@ -104,14 +100,6 @@ impl Error { Self::EvalError(pos, e.message) } - /// Creates a position-less value computation error. - // TODO(jmmv): This only exists because of some public operations in `Machine` that operate - // "out of band". We should probably remove those, put them elsewhere, and/or make them return - // a different error type rather than weakening what we store in `Error`. - fn from_value_error_without_pos(e: value::Error) -> Self { - Self::ValueError(e) - } - /// Returns true if this type of error can be caught by `ON ERROR`. fn is_catchable(&self) -> bool { match self { @@ -121,7 +109,6 @@ impl Error { Error::NestedError(_) => false, Error::ParseError(_) => false, Error::SyntaxError(..) => true, - Error::ValueError(_) => false, } } } @@ -652,45 +639,6 @@ impl Machine { &mut self.symbols } - /// Retrieves the variable `name` as a boolean. Fails if it is some other type or if it's not - /// defined. - pub fn get_var_as_bool(&self, name: &str) -> Result { - match self - .symbols - .get_var(&VarRef::new(name, Some(ExprType::Boolean))) - .map_err(Error::from_value_error_without_pos)? - { - Value::Boolean(b) => Ok(*b), - _ => panic!("Invalid type check in get()"), - } - } - - /// Retrieves the variable `name` as an integer. Fails if it is some other type or if it's not - /// defined. - pub fn get_var_as_int(&self, name: &str) -> Result { - match self - .symbols - .get_var(&VarRef::new(name, Some(ExprType::Integer))) - .map_err(Error::from_value_error_without_pos)? - { - Value::Integer(i) => Ok(*i), - _ => panic!("Invalid type check in get()"), - } - } - - /// Retrieves the variable `name` as a string. Fails if it is some other type or if it's not - /// defined. - pub fn get_var_as_string(&self, name: &str) -> Result<&str> { - match self - .symbols - .get_var(&VarRef::new(name, Some(ExprType::Text))) - .map_err(Error::from_value_error_without_pos)? - { - Value::Text(s) => Ok(s), - _ => panic!("Invalid type check in get()"), - } - } - /// Returns true if execution should stop because we have hit a stop condition. async fn should_stop(&mut self) -> bool { if let Some(yield_now) = self.yield_now_fn.as_ref() { @@ -1809,12 +1757,18 @@ mod tests { StopReason::Eof, block_on(machine.exec(&mut b"a = TRUE: b = 1".as_ref())).expect("Execution failed") ); - assert!(machine.get_var_as_bool("a").is_ok()); - assert!(machine.get_var_as_int("b").is_ok()); + match machine.get_symbols().get_auto("a") { + Some(Symbol::Variable(Value::Boolean(_))) => (), + e => panic!("a is not a bool: {:?}", e), + } + match machine.get_symbols().get_auto("b") { + Some(Symbol::Variable(Value::Integer(_))) => (), + e => panic!("a is not an integer: {:?}", e), + } assert!(!*cleared.borrow()); machine.clear(); - assert!(machine.get_var_as_bool("a").is_err()); - assert!(machine.get_var_as_int("b").is_err()); + assert!(machine.get_symbols().get_auto("a").is_none()); + assert!(machine.get_symbols().get_auto("b").is_none()); assert!(*cleared.borrow()); } @@ -1874,61 +1828,6 @@ mod tests { assert!(machine.get_data().is_empty()); } - #[test] - fn test_get_var_as_bool() { - let mut machine = Machine::default(); - assert_eq!( - StopReason::Eof, - block_on(machine.exec(&mut b"a = TRUE: b = 1".as_ref())).expect("Execution failed") - ); - assert!(machine.get_var_as_bool("a").expect("Failed to query a")); - assert_eq!( - "Incompatible types in b? reference", - format!("{}", machine.get_var_as_bool("b").expect_err("Querying b succeeded")) - ); - assert_eq!( - "Undefined variable c", - format!("{}", machine.get_var_as_bool("c").expect_err("Querying c succeeded")) - ); - } - - #[test] - fn test_get_var_as_int() { - let mut machine = Machine::default(); - assert_eq!( - StopReason::Eof, - block_on(machine.exec(&mut b"a = 1: b = \"foo\"".as_ref())).expect("Execution failed") - ); - assert_eq!(1, machine.get_var_as_int("a").expect("Failed to query a")); - assert_eq!( - "Incompatible types in b% reference", - format!("{}", machine.get_var_as_int("b").expect_err("Querying b succeeded")) - ); - assert_eq!( - "Undefined variable c", - format!("{}", machine.get_var_as_int("c").expect_err("Querying c succeeded")) - ); - } - - #[test] - fn test_get_var_as_string() { - let mut machine = Machine::default(); - assert_eq!( - StopReason::Eof, - block_on(machine.exec(&mut b"a = \"foo\": b = FALSE".as_ref())) - .expect("Execution failed") - ); - assert_eq!("foo", machine.get_var_as_string("a").expect("Failed to query a")); - assert_eq!( - "Incompatible types in b$ reference", - format!("{}", machine.get_var_as_string("b").expect_err("Querying b succeeded")) - ); - assert_eq!( - "Undefined variable c", - format!("{}", machine.get_var_as_string("c").expect_err("Querying c succeeded")) - ); - } - /// Runs the `input` code on a new test machine. /// /// `golden_in` is the sequence of values to yield by `IN`. @@ -3157,7 +3056,10 @@ mod tests { let mut machine = Machine::default(); assert_eq!(StopReason::Eof, block_on(machine.exec(&mut code.as_bytes())).unwrap()); assert_eq!(1, machine.get_symbols().locals().len()); - assert_eq!(4, machine.get_var_as_int("I").unwrap()); + match machine.get_symbols().get_auto("I") { + Some(Symbol::Variable(Value::Integer(i))) => assert_eq!(4, *i), + e => panic!("I is not an integer: {:?}", e), + } } #[test] diff --git a/core/src/syms.rs b/core/src/syms.rs index 0aba70a6..7f1f95af 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -436,6 +436,7 @@ impl Symbols { /// Returns an error if the variable is not defined, if the referenced symbol is not a variable, /// or if the type annotation in the variable reference does not match the type of the value /// that the variable contains. + #[cfg(test)] pub(crate) fn get_var(&self, vref: &VarRef) -> Result<&Value> { match self.get(vref)? { Some(Symbol::Variable(v)) => Ok(v), diff --git a/core/tests/config.out b/core/tests/config.out index c4249a40..eeb33439 100644 --- a/core/tests/config.out +++ b/core/tests/config.out @@ -1,3 +1,3 @@ foo_value is 123 enable_bar is true -enable_baz is not set: Undefined variable enable_baz +enable_baz is not set From b36e05efc02da50fc6fb43fba1b761d2b1a0413a Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 13 Oct 2024 15:23:04 -0700 Subject: [PATCH 004/110] Simplify next_pos tracking in the CharReader Fold the Peekable logic into CharReader so we don't have to wrap it in an iterator that we don't control. This lets us reach into the next_pos field without exposing it via an Rc+RefCell, which was always weird. --- core/src/lexer.rs | 16 ++++--------- core/src/reader.rs | 56 +++++++++++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/core/src/lexer.rs b/core/src/lexer.rs index 1e2cfb64..e98ebe6f 100644 --- a/core/src/lexer.rs +++ b/core/src/lexer.rs @@ -17,9 +17,6 @@ use crate::ast::{ExprType, VarRef}; use crate::reader::{CharReader, CharSpan, LineCol}; -use std::cell::RefCell; -use std::iter::Peekable; -use std::rc::Rc; use std::{fmt, io}; /// Collection of valid tokens. @@ -259,18 +256,13 @@ impl TokenSpan { /// Iterator over the tokens of the language. pub struct Lexer<'a> { /// Peekable iterator over the characters to scan. - input: Peekable>, - - next_pos_watcher: Rc>, + 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 { - let reader = CharReader::from(input); - let next_pos_watcher = reader.next_pos_watcher(); - let input = reader.peekable(); - Self { input, next_pos_watcher } + Self { input: CharReader::from(input) } } /// Handles an `input.next()` call that returned an unexpected character. @@ -640,7 +632,7 @@ impl<'a> Lexer<'a> { loop { match self.input.next() { None => { - let last_pos = *self.next_pos_watcher.borrow(); + 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' => { @@ -672,7 +664,7 @@ impl<'a> Lexer<'a> { pub fn read(&mut self) -> io::Result { let ch_span = self.advance_and_read_next()?; if ch_span.is_none() { - let last_pos = *self.next_pos_watcher.borrow(); + let last_pos = self.input.next_pos(); return Ok(TokenSpan::new(Token::Eof, last_pos, 0)); } let ch_span = ch_span.unwrap(); diff --git a/core/src/reader.rs b/core/src/reader.rs index 47908725..60f6194a 100644 --- a/core/src/reader.rs +++ b/core/src/reader.rs @@ -15,11 +15,9 @@ //! Character-based reader for an input stream with position tracking. -use std::cell::RefCell; use std::char; use std::fmt; use std::io::{self, BufRead}; -use std::rc::Rc; /// Tab length used to compute the current position within a line when encountering a tab character. const TAB_LENGTH: usize = 8; @@ -76,8 +74,12 @@ pub struct CharReader<'a> { /// 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: Rc>, + next_pos: LineCol, } impl<'a> CharReader<'a> { @@ -86,7 +88,8 @@ impl<'a> CharReader<'a> { Self { reader: io::BufReader::new(reader), pending: Pending::Unknown, - next_pos: Rc::from(RefCell::from(LineCol { line: 1, col: 1 })), + peeked: None, + next_pos: LineCol { line: 1, col: 1 }, } } @@ -103,10 +106,19 @@ impl<'a> CharReader<'a> { self.next() } - /// Obtains a view of the next position observed by this reader, which is necessary to compute - /// the location of EOF when the iterator is fully consumed. - pub(crate) fn next_pos_watcher(&self) -> Rc> { - self.next_pos.clone() + /// 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 } } @@ -114,6 +126,10 @@ impl<'a> Iterator for CharReader<'a> { 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, @@ -124,19 +140,18 @@ impl<'a> Iterator for CharReader<'a> { let ch = chars[*last]; *last += 1; - let mut next_pos = self.next_pos.borrow_mut(); - let pos = *next_pos; + let pos = self.next_pos; match ch { '\n' => { - next_pos.line += 1; - next_pos.col = 1; + self.next_pos.line += 1; + self.next_pos.col = 1; } '\t' => { - next_pos.col = - (next_pos.col - 1 + TAB_LENGTH) / TAB_LENGTH * TAB_LENGTH + 1; + self.next_pos.col = + (self.next_pos.col - 1 + TAB_LENGTH) / TAB_LENGTH * TAB_LENGTH + 1; } _ => { - next_pos.col += 1; + self.next_pos.col += 1; } } @@ -248,17 +263,16 @@ mod tests { } #[test] - fn test_next_pos_watcher() { + fn test_next_pos() { let mut input = "Hi".as_bytes(); let mut reader = CharReader::from(&mut input); - let next_pos_watcher = reader.next_pos_watcher(); - assert_eq!(LineCol { line: 1, col: 1 }, *next_pos_watcher.borrow()); + 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 }, *next_pos_watcher.borrow()); + 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 }, *next_pos_watcher.borrow()); + assert_eq!(LineCol { line: 1, col: 3 }, reader.next_pos()); assert!(reader.next().is_none()); - assert_eq!(LineCol { line: 1, col: 3 }, *next_pos_watcher.borrow()); + assert_eq!(LineCol { line: 1, col: 3 }, reader.next_pos()); } /// A reader that generates an error only on the Nth read operation. From 6b29d875a849c259170dbd14826470262f3626fa Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 13 Oct 2024 16:01:20 -0700 Subject: [PATCH 005/110] Track the position of I/O errors in parser/exec This is one step towards bringing orthogonality across error types and avoids having to track the position "out of band". --- cli/src/main.rs | 16 +++++++--------- core/src/exec.rs | 14 +++++++------- core/src/lexer.rs | 16 +++++++++++----- core/src/parser.rs | 10 ++++++++-- repl/src/lib.rs | 2 +- 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index d65adb26..90a1b184 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -132,7 +132,7 @@ fn make_interactive( fn finish_interactive_build( mut builder: endbasic_std::InteractiveMachineBuilder, service_url: &str, -) -> endbasic_core::exec::Result { +) -> Result { let console = builder.get_console(); let storage = builder.get_storage(); @@ -259,7 +259,7 @@ async fn run_repl_loop( console_spec: Option<&str>, local_drive_spec: &str, service_url: &str, -) -> endbasic_core::exec::Result { +) -> Result { let mut builder = make_interactive(new_machine_builder(console_spec)?); let console = builder.get_console(); @@ -275,10 +275,7 @@ async fn run_repl_loop( } /// Executes the `path` program in a fresh machine. -async fn run_script>( - path: P, - console_spec: Option<&str>, -) -> endbasic_core::exec::Result { +async fn run_script>(path: P, console_spec: Option<&str>) -> Result { let mut machine = new_machine_builder(console_spec)?.build()?; let mut input = File::open(path)?; Ok(machine.exec(&mut input).await?.as_exit_code()) @@ -297,7 +294,7 @@ async fn run_interactive( console_spec: Option<&str>, local_drive_spec: &str, service_url: &str, -) -> endbasic_core::exec::Result { +) -> Result { let mut builder = make_interactive(new_machine_builder(console_spec)?); let console = builder.get_console(); @@ -310,7 +307,7 @@ async fn run_interactive( match path.strip_prefix("cloud://") { Some(username_path) => { - endbasic_repl::run_from_cloud( + let code = endbasic_repl::run_from_cloud( &mut machine, console.clone(), storage.clone(), @@ -318,7 +315,8 @@ async fn run_interactive( username_path, false, ) - .await + .await?; + Ok(code) } None => { let mut input = File::open(path)?; diff --git a/core/src/exec.rs b/core/src/exec.rs index 3b463048..7f57aecb 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -43,8 +43,8 @@ pub enum Error { EvalError(LineCol, String), /// I/O error during execution. - #[error("{0}")] - IoError(#[from] io::Error), + #[error("{0}: {1}")] + IoError(LineCol, io::Error), /// Hack to support errors that arise from within a program that is `RUN`. // TODO(jmmv): Consider unifying `CallError` with `exec::Error`. @@ -80,10 +80,10 @@ impl Error { CallError::InternalError(pos2, e) => { Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) } - CallError::IoError(e) => Self::IoError(io::Error::new( - e.kind(), - format!("{}: In call to {}: {}", pos, md.name(), e), - )), + CallError::IoError(e) => Self::IoError( + pos, + io::Error::new(e.kind(), format!("In call to {}: {}", md.name(), e)), + ), CallError::NestedError(e) => Self::NestedError(e), CallError::SyntaxError if md.syntax().is_empty() => { Self::SyntaxError(pos, format!("In call to {}: expected no arguments", md.name())) @@ -105,7 +105,7 @@ impl Error { match self { Error::CompilerError(_) => false, Error::EvalError(..) => true, - Error::IoError(_) => true, + Error::IoError(..) => true, Error::NestedError(_) => false, Error::ParseError(_) => false, Error::SyntaxError(..) => true, diff --git a/core/src/lexer.rs b/core/src/lexer.rs index e98ebe6f..d1a98e1d 100644 --- a/core/src/lexer.rs +++ b/core/src/lexer.rs @@ -19,6 +19,9 @@ 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 @@ -733,10 +736,10 @@ impl<'a> PeekableLexer<'a> { /// /// It is OK to call this function several times on the same token before extracting it from /// the lexer. - pub fn peek(&mut self) -> io::Result<&TokenSpan> { + pub fn peek(&mut self) -> Result<&TokenSpan> { if self.peeked.is_none() { - let n = self.read()?; - self.peeked.replace(n); + let span = self.read()?; + self.peeked.replace(span); } Ok(self.peeked.as_ref().unwrap()) } @@ -745,10 +748,13 @@ impl<'a> PeekableLexer<'a> { /// /// 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) -> io::Result { + pub fn read(&mut self) -> Result { match self.peeked.take() { Some(t) => Ok(t), - None => self.lexer.read(), + None => match self.lexer.read() { + Ok(span) => Ok(span), + Err(e) => Err((self.lexer.input.next_pos(), e)), + }, } } } diff --git a/core/src/parser.rs b/core/src/parser.rs index a94304b5..2c87af20 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -29,8 +29,14 @@ pub enum Error { Bad(LineCol, String), /// I/O error while parsing the input program. - #[error("read error")] - Io(#[from] io::Error), + #[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. diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 2bb17f7a..ce63329e 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -89,7 +89,7 @@ pub async fn run_from_cloud( program: Rc>, username_path: &str, will_run_repl: bool, -) -> endbasic_core::exec::Result { +) -> io::Result { let (fs_uri, path) = match username_path.split_once('/') { Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)), None => { From 4c03e74ff0e1bc84a30e487ff2016974504e29f0 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 13 Oct 2024 16:28:52 -0700 Subject: [PATCH 006/110] Eliminate exec::Error::NestedError I'm still not too happy that CallError needs to have a NestedError variant, but exec::Error doesn't need it: we can just propagate the original error without flattening its representation as a string. --- core/src/exec.rs | 8 +------- core/src/syms.rs | 4 ++-- std/src/program.rs | 6 +++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/core/src/exec.rs b/core/src/exec.rs index 7f57aecb..851274c9 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -46,11 +46,6 @@ pub enum Error { #[error("{0}: {1}")] IoError(LineCol, io::Error), - /// Hack to support errors that arise from within a program that is `RUN`. - // TODO(jmmv): Consider unifying `CallError` with `exec::Error`. - #[error("{0}")] - NestedError(String), - /// Parsing error during execution. #[error("{0}")] ParseError(#[from] parser::Error), @@ -84,7 +79,7 @@ impl Error { pos, io::Error::new(e.kind(), format!("In call to {}: {}", md.name(), e)), ), - CallError::NestedError(e) => Self::NestedError(e), + CallError::NestedError(e) => e, CallError::SyntaxError if md.syntax().is_empty() => { Self::SyntaxError(pos, format!("In call to {}: expected no arguments", md.name())) } @@ -106,7 +101,6 @@ impl Error { Error::CompilerError(_) => false, Error::EvalError(..) => true, Error::IoError(..) => true, - Error::NestedError(_) => false, Error::ParseError(_) => false, Error::SyntaxError(..) => true, } diff --git a/core/src/syms.rs b/core/src/syms.rs index 7f1f95af..ac0b675f 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -17,7 +17,7 @@ use crate::ast::{ExprType, Value, VarRef}; use crate::compiler::{self, CallableSyntax, RepeatedSyntax, SingularArgSyntax}; -use crate::exec::{Machine, Scope}; +use crate::exec::{self, Machine, Scope}; use crate::reader::LineCol; use crate::value::{Error, Result}; use async_trait::async_trait; @@ -50,7 +50,7 @@ pub enum CallError { /// Hack to support errors that arise from within a program that is `RUN`. // TODO(jmmv): Consider unifying `CallError` with `exec::Error`. - NestedError(String), + NestedError(exec::Error), /// General mismatch of parameters given to the function with expectations (different numbers, /// invalid types). diff --git a/std/src/program.rs b/std/src/program.rs index 38940ff2..38454e05 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -21,7 +21,7 @@ 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::exec::{Machine, Scope, StopReason}; +use endbasic_core::exec::{self, Machine, Scope, StopReason}; use endbasic_core::parser::parse; use endbasic_core::syms::{ CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, @@ -192,7 +192,7 @@ impl Callable for DisasmCommand { let program = self.program.borrow_mut(); let ast = match parse(&mut program.text().as_bytes()) { Ok(ast) => ast, - Err(e) => return Err(CallError::NestedError(e.to_string())), + Err(e) => return Err(CallError::NestedError(exec::Error::ParseError(e))), }; compile(ast, machine.get_symbols())? }; @@ -517,7 +517,7 @@ impl Callable for RunCommand { let program = self.program.borrow().text(); let stop_reason = match machine.exec(&mut program.as_bytes()).await { Ok(stop_reason) => stop_reason, - Err(e) => return Err(CallError::NestedError(format!("{}", e))), + Err(e) => return Err(CallError::NestedError(e)), }; match stop_reason { StopReason::Break => self.console.borrow_mut().print(BREAK_MSG)?, From ca243400fde5df54badddb00061c47d610c7f6a5 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 12 Oct 2024 06:39:56 -0700 Subject: [PATCH 007/110] Unify compiler errors under an enum Instead of having a trivial string as the error message for all compilation errors, define an exhaustive enum of all possible problems. This helps highlight the many slight variants of error messages that existed for no good reason and helps unify those for clarity, cutting down on code duplication. --- cli/tests/cli/interactive.err | 2 +- cli/tests/lang/control-flow-errors.out | 6 +- cli/tests/lang/exprs-errors.out | 24 +- cli/tests/lang/operators.out | 50 ++--- cli/tests/lang/types.out | 8 +- cli/tests/repl/editor.out | 2 +- cli/tests/repl/interactive.out | 2 +- cli/tests/repl/state-sharing.out | 6 +- client/src/cmds.rs | 44 ++-- core/src/compiler/args.rs | 297 +++++++++++++------------ core/src/compiler/exprs.rs | 170 ++++---------- core/src/compiler/mod.rs | 235 +++++++++---------- core/src/exec.rs | 65 +++--- core/src/syms.rs | 100 ++++----- repl/src/lib.rs | 2 +- std/src/arrays.rs | 31 +-- std/src/console/cmds.rs | 105 +++------ std/src/data.rs | 14 +- std/src/exec.rs | 23 +- std/src/gfx.rs | 51 +---- std/src/gpio/mod.rs | 84 +++---- std/src/help.rs | 13 +- std/src/numerics.rs | 114 +++------- std/src/program.rs | 19 +- std/src/storage/cmds.rs | 36 +-- std/src/strings.rs | 99 +++------ std/tests/script-errors.err | 2 +- 27 files changed, 627 insertions(+), 977 deletions(-) diff --git a/cli/tests/cli/interactive.err b/cli/tests/cli/interactive.err index 4d8c257d..0200e56c 100644 --- a/cli/tests/cli/interactive.err +++ b/cli/tests/cli/interactive.err @@ -1 +1 @@ -endbasic: 20:1: Unknown builtin HELP +endbasic: 20:1: Undefined symbol HELP diff --git a/cli/tests/lang/control-flow-errors.out b/cli/tests/lang/control-flow-errors.out index 277bab0a..a2f1a8b9 100644 --- a/cli/tests/lang/control-flow-errors.out +++ b/cli/tests/lang/control-flow-errors.out @@ -4,9 +4,9 @@ Type HELP for interactive usage information. -ERROR: 1:4: IF/ELSEIF require a boolean condition -ERROR: 1:4: IF/ELSEIF require a boolean condition -ERROR: 1:15: Cannot compare BOOLEAN and BOOLEAN with <= +ERROR: 1:4: IF/ELSEIF requires a boolean condition +ERROR: 1:4: IF/ELSEIF requires a boolean condition +ERROR: 1:15: Cannot <= BOOLEAN and BOOLEAN ERROR: 1:7: WHILE requires a boolean condition ERROR: 1:16: LOOP requires a boolean condition ERROR: 1:16: LOOP requires a boolean condition diff --git a/cli/tests/lang/exprs-errors.out b/cli/tests/lang/exprs-errors.out index 13324d62..d96fdbf4 100644 --- a/cli/tests/lang/exprs-errors.out +++ b/cli/tests/lang/exprs-errors.out @@ -5,19 +5,19 @@ Type HELP for interactive usage information. >>> Symbol references -ERROR: 1:7: Undefined variable x +ERROR: 1:7: Undefined symbol X >>> Logical operations ERROR: 1:13: Cannot AND BOOLEAN and INTEGER ERROR: 1:13: Cannot OR BOOLEAN and INTEGER ERROR: 1:13: Cannot XOR BOOLEAN and INTEGER -ERROR: 1:7: Cannot apply NOT to DOUBLE +ERROR: 1:7: Cannot NOT DOUBLE >>> Relational operations -ERROR: 1:13: Cannot compare BOOLEAN and INTEGER with = -ERROR: 1:13: Cannot compare BOOLEAN and INTEGER with <> -ERROR: 1:13: Cannot compare BOOLEAN and INTEGER with < -ERROR: 1:13: Cannot compare BOOLEAN and INTEGER with <= -ERROR: 1:13: Cannot compare BOOLEAN and INTEGER with > -ERROR: 1:13: Cannot compare BOOLEAN and INTEGER with >= +ERROR: 1:13: Cannot = BOOLEAN and INTEGER +ERROR: 1:13: Cannot <> BOOLEAN and INTEGER +ERROR: 1:13: Cannot < BOOLEAN and INTEGER +ERROR: 1:13: Cannot <= BOOLEAN and INTEGER +ERROR: 1:13: Cannot > BOOLEAN and INTEGER +ERROR: 1:13: Cannot >= BOOLEAN and INTEGER >>> Arithmetic operations ERROR: 1:13: Cannot + BOOLEAN and INTEGER ERROR: 1:13: Cannot - BOOLEAN and INTEGER @@ -30,12 +30,12 @@ ERROR: 1:7: Cannot negate BOOLEAN ERROR: 1:13: Cannot + INTEGER and BOOLEAN >>> Array accesses ERROR: 1:7: Incompatible type annotation in x# reference -ERROR: 1:7: Unknown function or array y -ERROR: 1:7: Unknown function or array a +ERROR: 1:7: Undefined symbol Y +ERROR: 1:7: Undefined symbol A >>> Simple function calls ERROR: 1:7: Incompatible type annotation in MAX$ reference -ERROR: 1:7: Unknown function or array UNKNOWN +ERROR: 1:7: Undefined symbol UNKNOWN >>> Calling of non-functions -ERROR: 1:7: z is not an array nor a function +ERROR: 1:7: Z is not an array nor a function ERROR: 1:7: PRINT is not an array nor a function End of input by CTRL-D diff --git a/cli/tests/lang/operators.out b/cli/tests/lang/operators.out index fcefe48b..c4674138 100644 --- a/cli/tests/lang/operators.out +++ b/cli/tests/lang/operators.out @@ -42,7 +42,7 @@ ERROR: 1:11: Cannot OR DOUBLE and DOUBLE ERROR: 1:11: Cannot XOR DOUBLE and DOUBLE >>> Test bitwise NOT -1 -ERROR: 1:7: Cannot apply NOT to DOUBLE +ERROR: 1:7: Cannot NOT DOUBLE >>> Test bitwise shift left 12 -268435456 @@ -52,12 +52,12 @@ ERROR: 1:7: Cannot apply NOT to DOUBLE 0 0 0 -ERROR: 1:13: Cannot apply << to BOOLEAN -ERROR: 1:13: Cannot apply << to BOOLEAN -ERROR: 1:11: Cannot apply << to DOUBLE -ERROR: 1:13: Cannot apply << to STRING -ERROR: 1:9: Number of bits to << must be an INTEGER, not a BOOLEAN -ERROR: 1:9: Number of bits to << must be an INTEGER, not a DOUBLE +ERROR: 1:13: Cannot << BOOLEAN +ERROR: 1:13: Cannot << BOOLEAN +ERROR: 1:11: Cannot << DOUBLE +ERROR: 1:13: Cannot << STRING +ERROR: 1:9: expected INTEGER but found BOOLEAN +ERROR: 1:9: expected INTEGER but found DOUBLE ERROR: 1:9: Number of bits to << (-1) must be positive >>> Test bitwise shift right 3 @@ -72,17 +72,17 @@ ERROR: 1:9: Number of bits to << (-1) must be positive 0 -1 -1 -ERROR: 1:13: Cannot apply >> to BOOLEAN -ERROR: 1:13: Cannot apply >> to BOOLEAN -ERROR: 1:11: Cannot apply >> to DOUBLE -ERROR: 1:13: Cannot apply >> to STRING -ERROR: 1:9: Number of bits to >> must be an INTEGER, not a BOOLEAN -ERROR: 1:9: Number of bits to >> must be an INTEGER, not a DOUBLE +ERROR: 1:13: Cannot >> BOOLEAN +ERROR: 1:13: Cannot >> BOOLEAN +ERROR: 1:11: Cannot >> DOUBLE +ERROR: 1:13: Cannot >> STRING +ERROR: 1:9: expected INTEGER but found BOOLEAN +ERROR: 1:9: expected INTEGER but found DOUBLE ERROR: 1:9: Number of bits to >> (-1) must be positive >>> Test = types TRUE FALSE -ERROR: 1:12: Cannot compare BOOLEAN and INTEGER with = +ERROR: 1:12: Cannot = BOOLEAN and INTEGER TRUE FALSE TRUE @@ -93,11 +93,11 @@ TRUE FALSE TRUE FALSE -ERROR: 1:10: Cannot compare STRING and BOOLEAN with = +ERROR: 1:10: Cannot = STRING and BOOLEAN >>> Test <> types FALSE TRUE -ERROR: 1:12: Cannot compare BOOLEAN and INTEGER with <> +ERROR: 1:12: Cannot <> BOOLEAN and INTEGER FALSE TRUE FALSE @@ -108,9 +108,9 @@ FALSE TRUE FALSE TRUE -ERROR: 1:10: Cannot compare STRING and BOOLEAN with <> +ERROR: 1:10: Cannot <> STRING and BOOLEAN >>> Test < types -ERROR: 1:13: Cannot compare BOOLEAN and BOOLEAN with < +ERROR: 1:13: Cannot < BOOLEAN and BOOLEAN FALSE TRUE FALSE @@ -123,9 +123,9 @@ FALSE TRUE FALSE TRUE -ERROR: 1:10: Cannot compare STRING and BOOLEAN with < +ERROR: 1:10: Cannot < STRING and BOOLEAN >>> Test <= types -ERROR: 1:13: Cannot compare BOOLEAN and BOOLEAN with < +ERROR: 1:13: Cannot < BOOLEAN and BOOLEAN FALSE TRUE TRUE @@ -141,9 +141,9 @@ TRUE FALSE TRUE TRUE -ERROR: 1:10: Cannot compare STRING and BOOLEAN with <= +ERROR: 1:10: Cannot <= STRING and BOOLEAN >>> Test > types -ERROR: 1:13: Cannot compare BOOLEAN and BOOLEAN with > +ERROR: 1:13: Cannot > BOOLEAN and BOOLEAN FALSE TRUE FALSE @@ -156,9 +156,9 @@ FALSE TRUE FALSE TRUE -ERROR: 1:10: Cannot compare STRING and BOOLEAN with > +ERROR: 1:10: Cannot > STRING and BOOLEAN >>> Test >= types -ERROR: 1:13: Cannot compare BOOLEAN and BOOLEAN with > +ERROR: 1:13: Cannot > BOOLEAN and BOOLEAN FALSE TRUE TRUE @@ -174,7 +174,7 @@ TRUE FALSE TRUE TRUE -ERROR: 1:10: Cannot compare STRING and BOOLEAN with >= +ERROR: 1:10: Cannot >= STRING and BOOLEAN >>> Test + types ERROR: 1:13: Cannot + BOOLEAN and BOOLEAN 7.1 diff --git a/cli/tests/lang/types.out b/cli/tests/lang/types.out index ba5fb750..87be56d5 100644 --- a/cli/tests/lang/types.out +++ b/cli/tests/lang/types.out @@ -23,12 +23,12 @@ ERROR: 1:30: Incompatible type annotation in a2# reference 3 ERROR: 1:7: Incompatible type annotation in LEN$ reference >>> Invalid type access -ERROR: 1:14: i is not an array nor a function -ERROR: 1:7: In call to LEN: function requires arguments +ERROR: 1:14: I is not an array nor a function +ERROR: 1:7: LEN expected expr$ ERROR: 1:7: PRINT is not an array nor a function >>> Argless function calls 3.141592653589793 -ERROR: 1:7: In call to PI: expected no arguments nor parenthesis +ERROR: 1:7: PI expected no arguments >>> Function references -ERROR: 1:1: In call to INPUT: 1:7: PI is not an array nor a function +ERROR: 1:7: PI is not an array nor a function End of input by CTRL-D diff --git a/cli/tests/repl/editor.out b/cli/tests/repl/editor.out index 566ce7a2..2bc2079d 100644 --- a/cli/tests/repl/editor.out +++ b/cli/tests/repl/editor.out @@ -19,7 +19,7 @@ Resumed editing at the end of the file And now the editor displays the last-saved file name Current program LOCAL:test.bas has unsaved changes! [?1049h[?25l ESC Exit | | Ln 1, Col 1  -[?25hP[?25l ESC Exit | * | Ln 1, Col 2 [?25hR[?25l ESC Exit | * | Ln 1, Col 3 [?25hI[?25l ESC Exit | * | Ln 1, Col 4 [?25hN[?25l ESC Exit | * | Ln 1, Col 5 [?25hT[?25l ESC Exit | * | Ln 1, Col 6 [?25h [?25l ESC Exit | * | Ln 1, Col 7 [?25h"[?25l ESC Exit | * | Ln 1, Col 8 [?25hS[?25l ESC Exit | * | Ln 1, Col 9 [?25hh[?25l ESC Exit | * | Ln 1, Col 10 [?25ho[?25l ESC Exit | * | Ln 1, Col 11 [?25hu[?25l ESC Exit | * | Ln 1, Col 12 [?25hl[?25l ESC Exit | * | Ln 1, Col 13 [?25hd[?25l ESC Exit | * | Ln 1, Col 14 [?25h [?25l ESC Exit | * | Ln 1, Col 15 [?25hn[?25l ESC Exit | * | Ln 1, Col 16 [?25ho[?25l ESC Exit | * | Ln 1, Col 17 [?25ht[?25l ESC Exit | * | Ln 1, Col 18 [?25h [?25l ESC Exit | * | Ln 1, Col 19 [?25hb[?25l ESC Exit | * | Ln 1, Col 20 [?25he[?25l ESC Exit | * | Ln 1, Col 21 [?25h [?25l ESC Exit | * | Ln 1, Col 22 [?25hp[?25l ESC Exit | * | Ln 1, Col 23 [?25hr[?25l ESC Exit | * | Ln 1, Col 24 [?25hi[?25l ESC Exit | * | Ln 1, Col 25 [?25hn[?25l ESC Exit | * | Ln 1, Col 26 [?25ht[?25l ESC Exit | * | Ln 1, Col 27 [?25he[?25l ESC Exit | * | Ln 1, Col 28 [?25hd[?25l ESC Exit | * | Ln 1, Col 29 [?25h"[?25l ESC Exit | * | Ln 1, Col 30 [?25h[?25l ESC Exit | * | Ln 2, Col 1 [?25hI[?25l ESC Exit | * | Ln 2, Col 2 [?25hN[?25l ESC Exit | * | Ln 2, Col 3 [?25hV[?25l ESC Exit | * | Ln 2, Col 4 [?25hA[?25l ESC Exit | * | Ln 2, Col 5 [?25hL[?25l ESC Exit | * | Ln 2, Col 6 [?25hI[?25l ESC Exit | * | Ln 2, Col 7 [?25hD[?25l ESC Exit | * | Ln 2, Col 8 [?25h [?25l ESC Exit | * | Ln 2, Col 9 [?25hL[?25l ESC Exit | * | Ln 2, Col 10 [?25hI[?25l ESC Exit | * | Ln 2, Col 11 [?25hN[?25l ESC Exit | * | Ln 2, Col 12 [?25hE[?25l ESC Exit | * | Ln 2, Col 13 [?25h[?25l ESC Exit | * | Ln 3, Col 1 [?25hP[?25l ESC Exit | * | Ln 3, Col 2 [?25hR[?25l ESC Exit | * | Ln 3, Col 3 [?25hI[?25l ESC Exit | * | Ln 3, Col 4 [?25hN[?25l ESC Exit | * | Ln 3, Col 5 [?25hT[?25l ESC Exit | * | Ln 3, Col 6 [?25h [?25l ESC Exit | * | Ln 3, Col 7 [?25h"[?25l ESC Exit | * | Ln 3, Col 8 [?25hS[?25l ESC Exit | * | Ln 3, Col 9 [?25hh[?25l ESC Exit | * | Ln 3, Col 10 [?25ho[?25l ESC Exit | * | Ln 3, Col 11 [?25hu[?25l ESC Exit | * | Ln 3, Col 12 [?25hl[?25l ESC Exit | * | Ln 3, Col 13 [?25hd[?25l ESC Exit | * | Ln 3, Col 14 [?25h [?25l ESC Exit | * | Ln 3, Col 15 [?25hn[?25l ESC Exit | * | Ln 3, Col 16 [?25ho[?25l ESC Exit | * | Ln 3, Col 17 [?25ht[?25l ESC Exit | * | Ln 3, Col 18 [?25h [?25l ESC Exit | * | Ln 3, Col 19 [?25hb[?25l ESC Exit | * | Ln 3, Col 20 [?25he[?25l ESC Exit | * | Ln 3, Col 21 [?25h [?25l ESC Exit | * | Ln 3, Col 22 [?25hr[?25l ESC Exit | * | Ln 3, Col 23 [?25he[?25l ESC Exit | * | Ln 3, Col 24 [?25ha[?25l ESC Exit | * | Ln 3, Col 25 [?25hc[?25l ESC Exit | * | Ln 3, Col 26 [?25hh[?25l ESC Exit | * | Ln 3, Col 27 [?25he[?25l ESC Exit | * | Ln 3, Col 28 [?25hd[?25l ESC Exit | * | Ln 3, Col 29 [?25h"[?25l ESC Exit | * | Ln 3, Col 30 [?25h[?25l ESC Exit | * | Ln 4, Col 1 [?25h[?1049lERROR: 2:1: Unknown builtin INVALID +[?25hP[?25l ESC Exit | * | Ln 1, Col 2 [?25hR[?25l ESC Exit | * | Ln 1, Col 3 [?25hI[?25l ESC Exit | * | Ln 1, Col 4 [?25hN[?25l ESC Exit | * | Ln 1, Col 5 [?25hT[?25l ESC Exit | * | Ln 1, Col 6 [?25h [?25l ESC Exit | * | Ln 1, Col 7 [?25h"[?25l ESC Exit | * | Ln 1, Col 8 [?25hS[?25l ESC Exit | * | Ln 1, Col 9 [?25hh[?25l ESC Exit | * | Ln 1, Col 10 [?25ho[?25l ESC Exit | * | Ln 1, Col 11 [?25hu[?25l ESC Exit | * | Ln 1, Col 12 [?25hl[?25l ESC Exit | * | Ln 1, Col 13 [?25hd[?25l ESC Exit | * | Ln 1, Col 14 [?25h [?25l ESC Exit | * | Ln 1, Col 15 [?25hn[?25l ESC Exit | * | Ln 1, Col 16 [?25ho[?25l ESC Exit | * | Ln 1, Col 17 [?25ht[?25l ESC Exit | * | Ln 1, Col 18 [?25h [?25l ESC Exit | * | Ln 1, Col 19 [?25hb[?25l ESC Exit | * | Ln 1, Col 20 [?25he[?25l ESC Exit | * | Ln 1, Col 21 [?25h [?25l ESC Exit | * | Ln 1, Col 22 [?25hp[?25l ESC Exit | * | Ln 1, Col 23 [?25hr[?25l ESC Exit | * | Ln 1, Col 24 [?25hi[?25l ESC Exit | * | Ln 1, Col 25 [?25hn[?25l ESC Exit | * | Ln 1, Col 26 [?25ht[?25l ESC Exit | * | Ln 1, Col 27 [?25he[?25l ESC Exit | * | Ln 1, Col 28 [?25hd[?25l ESC Exit | * | Ln 1, Col 29 [?25h"[?25l ESC Exit | * | Ln 1, Col 30 [?25h[?25l ESC Exit | * | Ln 2, Col 1 [?25hI[?25l ESC Exit | * | Ln 2, Col 2 [?25hN[?25l ESC Exit | * | Ln 2, Col 3 [?25hV[?25l ESC Exit | * | Ln 2, Col 4 [?25hA[?25l ESC Exit | * | Ln 2, Col 5 [?25hL[?25l ESC Exit | * | Ln 2, Col 6 [?25hI[?25l ESC Exit | * | Ln 2, Col 7 [?25hD[?25l ESC Exit | * | Ln 2, Col 8 [?25h [?25l ESC Exit | * | Ln 2, Col 9 [?25hL[?25l ESC Exit | * | Ln 2, Col 10 [?25hI[?25l ESC Exit | * | Ln 2, Col 11 [?25hN[?25l ESC Exit | * | Ln 2, Col 12 [?25hE[?25l ESC Exit | * | Ln 2, Col 13 [?25h[?25l ESC Exit | * | Ln 3, Col 1 [?25hP[?25l ESC Exit | * | Ln 3, Col 2 [?25hR[?25l ESC Exit | * | Ln 3, Col 3 [?25hI[?25l ESC Exit | * | Ln 3, Col 4 [?25hN[?25l ESC Exit | * | Ln 3, Col 5 [?25hT[?25l ESC Exit | * | Ln 3, Col 6 [?25h [?25l ESC Exit | * | Ln 3, Col 7 [?25h"[?25l ESC Exit | * | Ln 3, Col 8 [?25hS[?25l ESC Exit | * | Ln 3, Col 9 [?25hh[?25l ESC Exit | * | Ln 3, Col 10 [?25ho[?25l ESC Exit | * | Ln 3, Col 11 [?25hu[?25l ESC Exit | * | Ln 3, Col 12 [?25hl[?25l ESC Exit | * | Ln 3, Col 13 [?25hd[?25l ESC Exit | * | Ln 3, Col 14 [?25h [?25l ESC Exit | * | Ln 3, Col 15 [?25hn[?25l ESC Exit | * | Ln 3, Col 16 [?25ho[?25l ESC Exit | * | Ln 3, Col 17 [?25ht[?25l ESC Exit | * | Ln 3, Col 18 [?25h [?25l ESC Exit | * | Ln 3, Col 19 [?25hb[?25l ESC Exit | * | Ln 3, Col 20 [?25he[?25l ESC Exit | * | Ln 3, Col 21 [?25h [?25l ESC Exit | * | Ln 3, Col 22 [?25hr[?25l ESC Exit | * | Ln 3, Col 23 [?25he[?25l ESC Exit | * | Ln 3, Col 24 [?25ha[?25l ESC Exit | * | Ln 3, Col 25 [?25hc[?25l ESC Exit | * | Ln 3, Col 26 [?25hh[?25l ESC Exit | * | Ln 3, Col 27 [?25he[?25l ESC Exit | * | Ln 3, Col 28 [?25hd[?25l ESC Exit | * | Ln 3, Col 29 [?25h"[?25l ESC Exit | * | Ln 3, Col 30 [?25h[?25l ESC Exit | * | Ln 4, Col 1 [?25h[?1049lERROR: 2:1: Undefined symbol INVALID Done. Current program has unsaved changes and has never been saved! End of input by CTRL-D diff --git a/cli/tests/repl/interactive.out b/cli/tests/repl/interactive.out index c0e76e37..0b88e4ab 100644 --- a/cli/tests/repl/interactive.out +++ b/cli/tests/repl/interactive.out @@ -5,6 +5,6 @@ Type HELP for interactive usage information. Got 123 from stdin -ERROR: 1:1: Unknown builtin FOOBAR +ERROR: 1:1: Undefined symbol FOOBAR Continuing End of input by CTRL-D diff --git a/cli/tests/repl/state-sharing.out b/cli/tests/repl/state-sharing.out index 8640649e..2fa7f21e 100644 --- a/cli/tests/repl/state-sharing.out +++ b/cli/tests/repl/state-sharing.out @@ -5,15 +5,15 @@ Type HELP for interactive usage information. 3 -ERROR: 1:7: Undefined variable a +ERROR: 1:7: Undefined symbol A [?1049h[?25l ESC Exit | | Ln 1, Col 1  [?25hD[?25l ESC Exit | * | Ln 1, Col 2 [?25hI[?25l ESC Exit | * | Ln 1, Col 3 [?25hM[?25l ESC Exit | * | Ln 1, Col 4 [?25h [?25l ESC Exit | * | Ln 1, Col 5 [?25ha[?25l ESC Exit | * | Ln 1, Col 6 [?25h([?25l ESC Exit | * | Ln 1, Col 7 [?25h1[?25l ESC Exit | * | Ln 1, Col 8 [?25h)[?25l ESC Exit | * | Ln 1, Col 9 [?25h [?25l ESC Exit | * | Ln 1, Col 10 [?25hA[?25l ESC Exit | * | Ln 1, Col 11 [?25hS[?25l ESC Exit | * | Ln 1, Col 12 [?25h [?25l ESC Exit | * | Ln 1, Col 13 [?25hI[?25l ESC Exit | * | Ln 1, Col 14 [?25hN[?25l ESC Exit | * | Ln 1, Col 15 [?25hT[?25l ESC Exit | * | Ln 1, Col 16 [?25hE[?25l ESC Exit | * | Ln 1, Col 17 [?25hG[?25l ESC Exit | * | Ln 1, Col 18 [?25hE[?25l ESC Exit | * | Ln 1, Col 19 [?25hR[?25l ESC Exit | * | Ln 1, Col 20 [?25h[?25l ESC Exit | * | Ln 2, Col 1 [?25ha[?25l ESC Exit | * | Ln 2, Col 2 [?25h([?25l ESC Exit | * | Ln 2, Col 3 [?25h0[?25l ESC Exit | * | Ln 2, Col 4 [?25h)[?25l ESC Exit | * | Ln 2, Col 5 [?25h [?25l ESC Exit | * | Ln 2, Col 6 [?25h=[?25l ESC Exit | * | Ln 2, Col 7 [?25h [?25l ESC Exit | * | Ln 2, Col 8 [?25h1[?25l ESC Exit | * | Ln 2, Col 9 [?25h2[?25l ESC Exit | * | Ln 2, Col 10 [?25h3[?25l ESC Exit | * | Ln 2, Col 11 [?25h[?25l ESC Exit | * | Ln 3, Col 1 [?25hP[?25l ESC Exit | * | Ln 3, Col 2 [?25hR[?25l ESC Exit | * | Ln 3, Col 3 [?25hI[?25l ESC Exit | * | Ln 3, Col 4 [?25hN[?25l ESC Exit | * | Ln 3, Col 5 [?25hT[?25l ESC Exit | * | Ln 3, Col 6 [?25h [?25l ESC Exit | * | Ln 3, Col 7 [?25h"[?25l ESC Exit | * | Ln 3, Col 8 [?25ha[?25l ESC Exit | * | Ln 3, Col 9 [?25h([?25l ESC Exit | * | Ln 3, Col 10 [?25h0[?25l ESC Exit | * | Ln 3, Col 11 [?25h)[?25l ESC Exit | * | Ln 3, Col 12 [?25h [?25l ESC Exit | * | Ln 3, Col 13 [?25hi[?25l ESC Exit | * | Ln 3, Col 14 [?25hs[?25l ESC Exit | * | Ln 3, Col 15 [?25h"[?25l ESC Exit | * | Ln 3, Col 16 [?25h;[?25l ESC Exit | * | Ln 3, Col 17 [?25h [?25l ESC Exit | * | Ln 3, Col 18 [?25ha[?25l ESC Exit | * | Ln 3, Col 19 [?25h([?25l ESC Exit | * | Ln 3, Col 20 [?25h0[?25l ESC Exit | * | Ln 3, Col 21 [?25h)[?25l ESC Exit | * | Ln 3, Col 22 [?25h[?25l ESC Exit | * | Ln 4, Col 1 [?25h[?1049la(0) is 123 a(0) is 123 a(0) is 123 a(0) before CLEAR is 123 -ERROR: 1:30: Unknown function or array a +ERROR: 1:30: Undefined symbol A a(0) is 123 a(0) before NEW is 123 Current program has unsaved changes and has never been saved! -ERROR: 1:28: Unknown function or array a +ERROR: 1:28: Undefined symbol A End of input by CTRL-D diff --git a/client/src/cmds.rs b/client/src/cmds.rs index 9d39770e..247360a8 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -747,31 +747,28 @@ mod tests { #[test] fn test_login_errors() { client_check_stmt_compilation_err( - "1:1: In call to LOGIN: expected | ", + "1:1: LOGIN expected | ", r#"LOGIN"#, ); client_check_stmt_compilation_err( - "1:1: In call to LOGIN: expected | ", + "1:1: LOGIN expected | ", r#"LOGIN "a", "b", "c""#, ); client_check_stmt_compilation_err( - "1:1: In call to LOGIN: expected | ", + "1:1: LOGIN expected | ", r#"LOGIN , "c""#, ); client_check_stmt_compilation_err( - "1:1: In call to LOGIN: expected | ", + "1:1: LOGIN expected | ", r#"LOGIN ;"#, ); + client_check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"LOGIN 3"#); client_check_stmt_compilation_err( - "1:1: In call to LOGIN: 1:7: INTEGER is not a STRING", - r#"LOGIN 3"#, - ); - client_check_stmt_compilation_err( - "1:1: In call to LOGIN: 1:7: INTEGER is not a STRING", + "1:7: expected STRING but found INTEGER", r#"LOGIN 3, "a""#, ); client_check_stmt_compilation_err( - "1:1: In call to LOGIN: 1:12: INTEGER is not a STRING", + "1:12: expected STRING but found INTEGER", r#"LOGIN "a", 3"#, ); } @@ -810,10 +807,7 @@ mod tests { #[test] fn test_logout_errors() { - client_check_stmt_compilation_err( - "1:1: In call to LOGOUT: expected no arguments", - r#"LOGOUT "a""#, - ); + client_check_stmt_compilation_err("1:1: LOGOUT expected no arguments", r#"LOGOUT "a""#); client_check_stmt_err("1:1: In call to LOGOUT: Must LOGIN first", r#"LOGOUT"#); } @@ -948,31 +942,28 @@ mod tests { #[test] fn test_share_errors() { client_check_stmt_compilation_err( - "1:1: In call to SHARE: expected filename$[, acl1$, .., aclN$]", + "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE"#, ); + client_check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"SHARE 1"#); client_check_stmt_compilation_err( - "1:1: In call to SHARE: 1:7: INTEGER is not a STRING", - r#"SHARE 1"#, - ); - client_check_stmt_compilation_err( - "1:1: In call to SHARE: expected filename$[, acl1$, .., aclN$]", + "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE , "a""#, ); client_check_stmt_compilation_err( - "1:1: In call to SHARE: expected filename$[, acl1$, .., aclN$]", + "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE "a"; "b""#, ); client_check_stmt_compilation_err( - "1:1: In call to SHARE: expected filename$[, acl1$, .., aclN$]", + "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE "a", "b"; "c""#, ); client_check_stmt_compilation_err( - "1:1: In call to SHARE: expected filename$[, acl1$, .., aclN$]", + "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE "a", , "b""#, ); client_check_stmt_compilation_err( - "1:1: In call to SHARE: 1:12: INTEGER is not a STRING", + "1:12: expected STRING but found INTEGER", r#"SHARE "a", 3, "b""#, ); client_check_stmt_err( @@ -1118,10 +1109,7 @@ mod tests { #[test] fn test_singup_errors() { - client_check_stmt_compilation_err( - "1:1: In call to SIGNUP: expected no arguments", - r#"SIGNUP "a""#, - ); + client_check_stmt_compilation_err("1:1: SIGNUP expected no arguments", r#"SIGNUP "a""#); } #[test] diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 0618166a..5731f9a4 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -18,16 +18,14 @@ use crate::ast::*; use crate::bytecode::*; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; -use crate::compiler::{ExprType, SymbolPrototype, SymbolsTable}; +use crate::compiler::{Error, ExprType, Result, SymbolPrototype, SymbolsTable}; use crate::exec::ValueTag; use crate::reader::LineCol; -use crate::syms::{CallError, SymbolKey}; +use crate::syms::CallableMetadata; +use crate::syms::SymbolKey; use std::borrow::Cow; use std::ops::RangeInclusive; -/// Result for argument compilation return values. -type Result = std::result::Result; - /// Details to compile a required scalar parameter. #[derive(Clone, Debug)] pub struct RequiredValueSyntax { @@ -266,6 +264,11 @@ impl CallableSyntax { 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(); @@ -326,6 +329,8 @@ impl CallableSyntax { /// not have mutable access to the `symtable` here. fn compile_required_ref( instrs: &mut Vec, + md: &CallableMetadata, + pos: LineCol, symtable: &SymbolsTable, require_array: bool, define_undefined: bool, @@ -337,21 +342,15 @@ fn compile_required_ref( match symtable.get(&key) { None => { if !define_undefined { - let message = if require_array { - format!("Undefined array {}", span.vref.name()) - } else { - format!("Undefined variable {}", span.vref.name()) - }; - return Err(CallError::ArgumentError(span.pos, message)); + return Err(Error::UndefinedSymbol(span.pos, key)); } debug_assert!(!require_array); let vtype = span.vref.ref_type().unwrap_or(ExprType::Integer); if !span.vref.accepts(vtype) { - return Err(CallError::ArgumentError( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), + return Err(Error::IncompatibleTypeAnnotationInReference( + span.pos, span.vref, )); } @@ -363,17 +362,13 @@ fn compile_required_ref( let vtype = *vtype; if !span.vref.accepts(vtype) { - return Err(CallError::ArgumentError( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), + return Err(Error::IncompatibleTypeAnnotationInReference( + span.pos, span.vref, )); } if !require_array { - return Err(CallError::ArgumentError( - span.pos, - format!("{} is not a variable reference", span.vref), - )); + return Err(Error::NotAReference(span.pos)); } instrs.push(Instruction::LoadRef(key, vtype, span.pos)); @@ -384,17 +379,13 @@ fn compile_required_ref( let vtype = *vtype; if !span.vref.accepts(vtype) { - return Err(CallError::ArgumentError( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), + return Err(Error::IncompatibleTypeAnnotationInReference( + span.pos, span.vref, )); } if require_array { - return Err(CallError::ArgumentError( - span.pos, - format!("{} is not an array reference", span.vref), - )); + return Err(Error::NotAReference(span.pos)); } instrs.push(Instruction::LoadRef(key, vtype, span.pos)); @@ -403,43 +394,34 @@ fn compile_required_ref( Some(SymbolPrototype::Callable(md)) => { if !span.vref.accepts_callable(md.return_type()) { - return Err(CallError::ArgumentError( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), + return Err(Error::IncompatibleTypeAnnotationInReference( + span.pos, span.vref, )); } - Err(CallError::ArgumentError( - span.pos, - format!("{} is not an array nor a function", span.vref.name()), - )) + Err(Error::NotArrayOrFunction(span.pos, key)) } } } - Some(expr) => { - let message = if require_array { - "Requires an array reference, not a value" - } else { - "Requires a variable reference, not a value" - }; - Err(CallError::ArgumentError(expr.start_pos(), message.to_owned())) - } + Some(expr) => Err(Error::NotAReference(expr.start_pos())), - None => Err(CallError::SyntaxError), + None => Err(Error::CallableSyntaxError(pos, md.clone())), } } /// Locates the syntax definition that can parse the given number of arguments. /// /// Panics if more than one syntax definition applies. -fn find_syntax(syntaxes: &[CallableSyntax], nargs: usize) -> Result<&CallableSyntax> { - let mut matches = syntaxes.iter().filter(|s| s.expected_nargs().contains(&nargs)); +fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&CallableSyntax> { + let mut matches = md.syntaxes().iter().filter(|s| s.expected_nargs().contains(&nargs)); let syntax = matches.next(); - debug_assert!(matches.next().is_none(), "Ambiguous syntax definitions"); match syntax { - Some(syntax) => Ok(syntax), - None => Err(CallError::SyntaxError), + Some(syntax) => { + debug_assert!(matches.next().is_none(), "Ambiguous syntax definitions"); + Ok(syntax) + } + None => Err(Error::CallableSyntaxError(pos, md.clone())), } } @@ -454,8 +436,11 @@ fn find_syntax(syntaxes: &[CallableSyntax], nargs: usize) -> Result<&CallableSyn /// /// `is_last` indicates whether this is the last separator in the command call and is used /// only for diagnostics purposes. +#[allow(clippy::too_many_arguments)] fn compile_syn_argsep( instrs: &mut Vec, + md: &CallableMetadata, + pos: LineCol, syn: &ArgSepSyntax, is_last: bool, sep: ArgSep, @@ -471,7 +456,7 @@ fn compile_syn_argsep( ArgSepSyntax::Exactly(exp_sep) => { debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); if sep != ArgSep::End && sep != *exp_sep { - return Err(CallError::SyntaxError); + return Err(Error::CallableSyntaxError(pos, md.clone())); } Ok(0) } @@ -483,7 +468,7 @@ fn compile_syn_argsep( Ok(0) } else { if sep != *exp_sep1 && sep != *exp_sep2 { - return Err(CallError::SyntaxError); + return Err(Error::CallableSyntaxError(pos, md.clone())); } instrs.insert(sep_tag_pc, Instruction::PushInteger(sep as i32, sep_pos)); Ok(1) @@ -497,32 +482,18 @@ fn compile_syn_argsep( } } -/// Compiles a single expression, expecting it to be of a `target` type. Applies casts if -/// possible. -fn compile_arg_expr( - instrs: &mut Vec, - symtable: &SymbolsTable, - expr: Expr, - target: ExprType, -) -> Result<()> { - match compile_expr_as_type(instrs, symtable, expr, target) { - Ok(()) => Ok(()), - Err(e) => Err(CallError::ArgumentError(e.pos, e.message)), - } -} - /// Parses the arguments to a command or a function and generates expressions to compute them. /// /// Returns the number of arguments that the instructions added to `instrs` will push into the /// stack and returns the list of new symbols that need to be inserted into `symtable`. fn compile_args( - syntaxes: &[CallableSyntax], + md: &CallableMetadata, instrs: &mut Vec, symtable: &SymbolsTable, - _pos: LineCol, + pos: LineCol, args: Vec, ) -> Result<(usize, Vec<(SymbolKey, SymbolPrototype)>)> { - let syntax = find_syntax(syntaxes, args.len())?; + let syntax = find_syntax(md, pos, args.len())?; let input_nargs = args.len(); let mut aiter = args.into_iter().rev(); @@ -537,7 +508,7 @@ fn compile_args( min_nargs += 1; } if input_nargs < min_nargs { - return Err(CallError::SyntaxError); + return Err(Error::CallableSyntaxError(pos, md.clone())); } let need_tags = syn.allow_missing || matches!(syn.type_syn, RepeatedTypeSyntax::AnyValue); @@ -561,8 +532,15 @@ fn compile_args( } RepeatedTypeSyntax::VariableRef => { - let to_insert_one = - compile_required_ref(instrs, symtable, false, true, Some(expr))?; + let to_insert_one = compile_required_ref( + instrs, + md, + pos, + symtable, + false, + true, + Some(expr), + )?; if let Some(to_insert_one) = to_insert_one { to_insert.push(to_insert_one); } @@ -570,7 +548,7 @@ fn compile_args( } RepeatedTypeSyntax::TypedValue(vtype) => { - compile_arg_expr(instrs, symtable, expr, vtype)?; + compile_expr_as_type(instrs, symtable, expr, vtype)?; if need_tags { instrs.push(Instruction::PushInteger( ValueTag::from(vtype) as i32, @@ -585,7 +563,7 @@ fn compile_args( } None => { if !syn.allow_missing { - return Err(CallError::SyntaxError); + return Err(Error::CallableSyntaxError(pos, md.clone())); } instrs.push(Instruction::PushInteger(ValueTag::Missing as i32, span.sep_pos)); nargs += 1; @@ -594,6 +572,8 @@ fn compile_args( nargs += compile_syn_argsep( instrs, + md, + pos, &syn.sep, input_nargs == remaining, span.sep, @@ -616,10 +596,10 @@ fn compile_args( SingularArgSyntax::RequiredValue(details, sep) => { match span.expr { Some(expr) => { - compile_arg_expr(instrs, symtable, expr, details.vtype)?; + compile_expr_as_type(instrs, symtable, expr, details.vtype)?; nargs += 1; } - None => return Err(CallError::SyntaxError), + None => return Err(Error::CallableSyntaxError(pos, md.clone())), } sep } @@ -627,6 +607,8 @@ fn compile_args( SingularArgSyntax::RequiredRef(details, sep) => { let to_insert_one = compile_required_ref( instrs, + md, + pos, symtable, details.require_array, details.define_undefined, @@ -643,7 +625,7 @@ fn compile_args( let (tag, pos) = match span.expr { Some(expr) => { let pos = expr.start_pos(); - compile_arg_expr(instrs, symtable, expr, details.vtype)?; + compile_expr_as_type(instrs, symtable, expr, details.vtype)?; nargs += 1; (details.present_value, pos) } @@ -664,10 +646,7 @@ fn compile_args( } None => { if !details.allow_missing { - return Err(CallError::ArgumentError( - span.sep_pos, - "Missing expression before separator".to_owned(), - )); + return Err(Error::CallableSyntaxError(span.sep_pos, md.clone())); } nargs += 1; (ValueTag::Missing, span.sep_pos) @@ -680,6 +659,8 @@ fn compile_args( nargs += compile_syn_argsep( instrs, + md, + pos, exp_sep, input_nargs == remaining, span.sep, @@ -698,13 +679,13 @@ fn compile_args( /// This can be used to help the runtime by doing type checking during compilation and then /// allowing the runtime to assume that the values on the stack are correctly typed. pub(super) fn compile_command_args( - syntaxes: &[CallableSyntax], + md: &CallableMetadata, instrs: &mut Vec, symtable: &mut SymbolsTable, pos: LineCol, args: Vec, ) -> Result { - let (nargs, to_insert) = compile_args(syntaxes, instrs, symtable, pos, args)?; + let (nargs, to_insert) = compile_args(md, instrs, symtable, pos, args)?; for (key, proto) in to_insert { if !symtable.contains_key(&key) { symtable.insert(key, proto); @@ -718,19 +699,21 @@ pub(super) fn compile_command_args( /// This can be used to help the runtime by doing type checking during compilation and then /// allowing the runtime to assume that the values on the stack are correctly typed. pub(super) fn compile_function_args( - syntaxes: &[CallableSyntax], + md: &CallableMetadata, instrs: &mut Vec, symtable: &SymbolsTable, pos: LineCol, args: Vec, ) -> Result { - let (nargs, to_insert) = compile_args(syntaxes, instrs, symtable, pos, args)?; + let (nargs, to_insert) = compile_args(md, instrs, symtable, pos, args)?; debug_assert!(to_insert.is_empty()); Ok(nargs) } #[cfg(test)] mod testutils { + use crate::syms::CallableMetadataBuilder; + use super::*; use std::collections::HashMap; @@ -772,13 +755,9 @@ mod testutils { // Start with one instruction to validate that the args compiler doesn't touch it. Instruction::Nop, ]; - let result = compile_command_args( - &self.syntaxes, - &mut instrs, - &mut self.symtable, - lc(1000, 2000), - args, - ); + let md = CallableMetadataBuilder::new("TEST").with_syntaxes(self.syntaxes).test_build(); + let result = + compile_command_args(&md, &mut instrs, &mut self.symtable, lc(1000, 2000), args); Checker { result, instrs, @@ -809,7 +788,7 @@ mod testutils { } /// Expects the compilation to fail with the given `error`. - pub(super) fn exp_error(mut self, error: CallError) -> Self { + pub(super) fn exp_error(mut self, error: Error) -> Self { self.exp_result = Err(error); self } @@ -826,24 +805,12 @@ mod testutils { self } - /// Formats a `CallError` as a string to simplify comparisons. - fn format_call_error(e: CallError) -> String { - match e { - CallError::ArgumentError(pos, e) => format!("{}: {}", pos, e), - CallError::EvalError(pos, e) => format!("{}: {}", pos, e), - CallError::InternalError(_pos, e) => panic!("Must not happen here: {}", e), - CallError::IoError(e) => panic!("Must not happen here: {}", e), - CallError::NestedError(e) => panic!("Must not happen here: {}", e), - CallError::SyntaxError => "Syntax error".to_owned(), - } - } - /// Checks that the compilation ended with the configured expectations. pub(super) fn check(self) { let is_ok = self.result.is_ok(); assert_eq!( - self.exp_result.map_err(Self::format_call_error), - self.result.map_err(Self::format_call_error), + self.exp_result.map_err(|e| format!("{}", e)), + self.result.map_err(|e| format!("{}", e)), ); if !is_ok { @@ -1121,11 +1088,7 @@ mod description_tests { mod compile_tests { use super::testutils::*; use super::*; - - #[test] - fn test_no_syntaxes_yields_error() { - Tester::default().compile_command([]).exp_error(CallError::SyntaxError).check(); - } + use crate::syms::CallableMetadataBuilder; #[test] fn test_no_args_ok() { @@ -1141,7 +1104,10 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 3), }]) - .exp_error(CallError::SyntaxError) + .exp_error(Error::CallableSyntaxError( + lc(1000, 2000), + CallableMetadataBuilder::new("TEST").test_build(), + )) .check(); } @@ -1236,7 +1202,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError(lc(1, 2), "Undefined variable foo".to_owned())) + .exp_error(Error::UndefinedSymbol(lc(1, 2), SymbolKey::from("foo"))) .check(); } @@ -1259,10 +1225,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError( - lc(1, 2), - "Requires a variable reference, not a value".to_owned(), - )) + .exp_error(Error::NotAReference(lc(1, 2))) .check(); } @@ -1289,10 +1252,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError( - lc(1, 2), - "foo is not a variable reference".to_owned(), - )) + .exp_error(Error::NotAReference(lc(1, 2))) .check(); } @@ -1319,9 +1279,9 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError( + .exp_error(Error::IncompatibleTypeAnnotationInReference( lc(1, 2), - "Incompatible type annotation in foo% reference".to_owned(), + VarRef::new("foo", Some(ExprType::Integer)), )) .check(); } @@ -1481,7 +1441,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError(lc(1, 2), "Undefined array foo".to_owned())) + .exp_error(Error::UndefinedSymbol(lc(1, 2), SymbolKey::from("foo"))) .check(); } @@ -1504,10 +1464,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError( - lc(1, 2), - "Requires an array reference, not a value".to_owned(), - )) + .exp_error(Error::NotAReference(lc(1, 2))) .check(); } @@ -1534,10 +1491,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError( - lc(1, 2), - "foo is not an array reference".to_owned(), - )) + .exp_error(Error::NotAReference(lc(1, 2))) .check(); } @@ -1564,9 +1518,9 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(CallError::ArgumentError( + .exp_error(Error::IncompatibleTypeAnnotationInReference( lc(1, 2), - "Incompatible type annotation in foo% reference".to_owned(), + VarRef::new("foo", Some(ExprType::Integer)), )) .check(); } @@ -1695,9 +1649,9 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 3), }]) - .exp_error(CallError::ArgumentError( + .exp_error(Error::IncompatibleTypeAnnotationInReference( lc(1, 2), - "Incompatible type annotation in foo? reference".to_owned(), + VarRef::new("foo", Some(ExprType::Boolean)), )) .check(); } @@ -1714,9 +1668,17 @@ mod compile_tests { None, ) .compile_command([ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 3) }]) - .exp_error(CallError::ArgumentError( + .exp_error(Error::CallableSyntaxError( lc(1, 3), - "Missing expression before separator".to_owned(), + CallableMetadataBuilder::new("TEST") + .with_syntax(&[( + &[SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg1"), allow_missing: false }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), )) .check(); } @@ -1797,7 +1759,24 @@ mod compile_tests { ArgSpan { expr: None, sep: ArgSep::Short, sep_pos: lc(1, 1) }, ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 4) }, ]) - .exp_error(CallError::SyntaxError) + .exp_error(Error::CallableSyntaxError( + lc(1000, 2000), + CallableMetadataBuilder::new("TEST") + .with_syntax(&[( + &[ + SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg1"), allow_missing: true }, + ArgSepSyntax::Exactly(ArgSep::As), + ), + SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg2"), allow_missing: true }, + ArgSepSyntax::End, + ), + ], + None, + )]) + .test_build(), + )) .check(); } @@ -1821,7 +1800,24 @@ mod compile_tests { ArgSpan { expr: None, sep: ArgSep::As, sep_pos: lc(1, 1) }, ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 4) }, ]) - .exp_error(CallError::SyntaxError) + .exp_error(Error::CallableSyntaxError( + lc(1000, 2000), + CallableMetadataBuilder::new("TEST") + .with_syntax(&[( + &[ + SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg1"), allow_missing: true }, + ArgSepSyntax::OneOf(ArgSep::Short, ArgSep::Long), + ), + SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg2"), allow_missing: true }, + ArgSepSyntax::End, + ), + ], + None, + )]) + .test_build(), + )) .check(); } @@ -1912,7 +1908,21 @@ mod compile_tests { }), ) .compile_command([]) - .exp_error(CallError::SyntaxError) + .exp_error(Error::CallableSyntaxError( + lc(1000, 2000), + CallableMetadataBuilder::new("TEST") + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::TypedValue(ExprType::Integer), + sep: ArgSepSyntax::Exactly(ArgSep::Long), + allow_missing: false, + require_one: true, + }), + )]) + .test_build(), + )) .check(); } @@ -1960,10 +1970,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 2), }]) - .exp_error(CallError::ArgumentError( - lc(1, 2), - "Requires a variable reference, not a value".to_owned(), - )) + .exp_error(Error::NotAReference(lc(1, 2))) .check(); } diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 61615b2a..3350aa7a 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -32,10 +32,7 @@ pub(super) fn compile_array_indices( name_pos: LineCol, ) -> Result<()> { if exp_nargs != args.len() { - return Err(Error::new( - name_pos, - format!("Cannot index array with {} subscripts; need {}", args.len(), exp_nargs), - )); + return Err(Error::ArrayIndexSubscriptsError(name_pos, args.len(), exp_nargs)); } for arg in args.into_iter().rev() { @@ -46,10 +43,7 @@ pub(super) fn compile_array_indices( instrs.push(Instruction::DoubleToInteger); } itype => { - return Err(Error::new( - arg_pos, - format!("Array index must be INTEGER, not {}", itype), - )); + return Err(Error::NotANumber(arg_pos, itype)); } } } @@ -73,7 +67,7 @@ fn compile_not_op( instrs.push(Instruction::BitwiseNot(span.pos)); Ok(ExprType::Integer) } - _ => Err(Error::new(span.pos, format!("Cannot apply NOT to {}", expr_type))), + _ => Err(Error::UnaryOpTypeError(span.pos, "NOT", expr_type)), } } @@ -93,7 +87,7 @@ fn compile_neg_op( instrs.push(Instruction::NegateInteger(span.pos)); Ok(ExprType::Integer) } - _ => Err(Error::new(span.pos, format!("Cannot negate {}", expr_type))), + _ => Err(Error::UnaryOpTypeError(span.pos, "negate", expr_type)), } } @@ -104,7 +98,7 @@ fn compile_logical_binary_op Instruction, F2: Fn(LineCol) -> logical_make_inst: F1, bitwise_make_inst: F2, span: BinaryOpSpan, - op_name: &str, + op_name: &'static str, ) -> Result { let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; let rhs_type = compile_expr(instrs, symtable, span.rhs, false)?; @@ -117,9 +111,7 @@ fn compile_logical_binary_op Instruction, F2: Fn(LineCol) -> instrs.push(bitwise_make_inst(span.pos)); Ok(ExprType::Integer) } - (_, _) => { - Err(Error::new(span.pos, format!("Cannot {} {} and {}", op_name, lhs_type, rhs_type))) - } + (_, _) => Err(Error::BinaryOpTypeError(span.pos, op_name, lhs_type, rhs_type)), } } @@ -138,7 +130,7 @@ fn compile_equality_binary_op< integer_make_inst: F3, text_make_inst: F4, span: BinaryOpSpan, - op_name: &str, + op_name: &'static str, ) -> Result { let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; let pc = instrs.len(); @@ -161,10 +153,7 @@ fn compile_equality_binary_op< } (_, _) => { - return Err(Error::new( - span.pos, - format!("Cannot compare {} and {} with {}", lhs_type, rhs_type, op_name), - )); + return Err(Error::BinaryOpTypeError(span.pos, op_name, lhs_type, rhs_type)); } }; @@ -195,7 +184,7 @@ fn compile_relational_binary_op< integer_make_inst: F2, text_make_inst: F3, span: BinaryOpSpan, - op_name: &str, + op_name: &'static str, ) -> Result { let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; let pc = instrs.len(); @@ -221,10 +210,7 @@ fn compile_relational_binary_op< } (_, _) => { - return Err(Error::new( - span.pos, - format!("Cannot compare {} and {} with {}", lhs_type, rhs_type, op_name), - )); + return Err(Error::BinaryOpTypeError(span.pos, op_name, lhs_type, rhs_type)); } }; @@ -249,13 +235,13 @@ fn compile_shift_binary_op Instruction>( symtable: &SymbolsTable, make_inst: F, span: BinaryOpSpan, - op_name: &str, + op_name: &'static str, ) -> Result { let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; match lhs_type { ExprType::Integer => (), _ => { - return Err(Error::new(span.pos, format!("Cannot apply {} to {}", op_name, lhs_type))); + return Err(Error::UnaryOpTypeError(span.pos, op_name, lhs_type)); } }; @@ -263,15 +249,7 @@ fn compile_shift_binary_op Instruction>( match rhs_type { ExprType::Integer => (), _ => { - return Err(Error::new( - span.pos, - format!( - "Number of bits to {} must be an {}, not a {}", - op_name, - ExprType::Integer, - rhs_type - ), - )); + return Err(Error::TypeMismatch(span.pos, rhs_type, ExprType::Integer)); } }; @@ -288,7 +266,7 @@ fn compile_arithmetic_binary_op Instruction, F2: Fn(LineCol) double_make_inst: F1, integer_make_inst: F2, span: BinaryOpSpan, - op_name: &str, + op_name: &'static str, ) -> Result { let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; let pc = instrs.len(); @@ -313,10 +291,7 @@ fn compile_arithmetic_binary_op Instruction, F2: Fn(LineCol) } (_, _) => { - return Err(Error::new( - span.pos, - format!("Cannot {} {} and {}", op_name, lhs_type, rhs_type), - )); + return Err(Error::BinaryOpTypeError(span.pos, op_name, lhs_type, rhs_type)); } }; @@ -347,18 +322,13 @@ fn compile_expr_symbol( ) -> Result { let key = SymbolKey::from(span.vref.name()); let (instr, vtype) = match symtable.get(&key) { - None => { - return Err(Error::new(span.pos, format!("Undefined variable {}", span.vref.name()))) - } + None => return Err(Error::UndefinedSymbol(span.pos, key)), Some(SymbolPrototype::Array(atype, _dims)) => { if allow_varrefs { (Instruction::LoadRef(key, *atype, span.pos), *atype) } else { - return Err(Error::new( - span.pos, - format!("{} is not a variable", span.vref.name()), - )); + return Err(Error::NotAVariable(span.pos, span.vref)); } } @@ -380,31 +350,21 @@ fn compile_expr_symbol( let etype = match md.return_type() { Some(etype) => etype, None => { - return Err(Error::new( - span.pos, - format!("{} is not an array nor a function", span.vref.name()), - )); + return Err(Error::NotArrayOrFunction(span.pos, key)); } }; if !md.is_argless() { - return Err(Error::new( - span.pos, - format!("In call to {}: function requires arguments", span.vref.name()), - )); + return Err(Error::CallableSyntaxError(span.pos, md.clone())); } - let nargs = compile_function_args(md.syntaxes(), instrs, symtable, span.pos, vec![]) - .map_err(|e| Error::from_call_error(md, e, span.pos))?; + let nargs = compile_function_args(md, instrs, symtable, span.pos, vec![])?; debug_assert_eq!(0, nargs, "Argless compiler must have returned zero arguments"); (Instruction::FunctionCall(key, etype, span.pos, 0), etype) } }; if !span.vref.accepts(vtype) { - return Err(Error::new( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); } instrs.push(instr); Ok(vtype) @@ -422,10 +382,7 @@ fn compile_expr_symbol_ref( let vtype = span.vref.ref_type().unwrap_or(ExprType::Integer); if !span.vref.accepts(vtype) { - return Err(Error::new( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); } symtable.insert(key.clone(), SymbolPrototype::Variable(vtype)); @@ -437,10 +394,7 @@ fn compile_expr_symbol_ref( let vtype = *vtype; if !span.vref.accepts(vtype) { - return Err(Error::new( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); } instrs.push(Instruction::LoadRef(key, vtype, span.pos)); @@ -449,16 +403,10 @@ fn compile_expr_symbol_ref( Some(SymbolPrototype::Callable(md)) => { if !span.vref.accepts_callable(md.return_type()) { - return Err(Error::new( - span.pos, - format!("Incompatible type annotation in {} reference", span.vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); } - Err(Error::new( - span.pos, - format!("{} is not an array nor a function", span.vref.name()), - )) + Err(Error::NotArrayOrFunction(span.pos, key)) } } } @@ -477,10 +425,7 @@ fn compile_array_ref( compile_array_indices(instrs, symtable, dimensions, exprs, span.vref_pos)?; if !span.vref.accepts(vtype) { - return Err(Error::new( - span.vref_pos, - format!("Incompatible type annotation in {} reference", span.vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(span.vref_pos, span.vref)); } instrs.push(Instruction::ArrayLoad(key, span.vref_pos, nargs)); Ok(vtype) @@ -715,49 +660,34 @@ pub(super) fn compile_expr( Some(SymbolPrototype::Callable(md)) => { if !span.vref.accepts_callable(md.return_type()) { - return Err(Error::new( + return Err(Error::IncompatibleTypeAnnotationInReference( span.vref_pos, - format!("Incompatible type annotation in {} reference", span.vref), + span.vref, )); } let vtype = match md.return_type() { Some(vtype) => vtype, None => { - return Err(Error::new( - span.vref_pos, - format!("{} is not an array nor a function", span.vref.name()), - )); + return Err(Error::NotArrayOrFunction(span.vref_pos, key)); } }; if md.is_argless() { - return Err(Error::new( - span.vref_pos, - format!( - "In call to {}: expected no arguments nor parenthesis", - span.vref.name() - ), - )); + return Err(Error::CallableSyntaxError(span.vref_pos, md.clone())); } let span_pos = span.vref_pos; - let nargs = - compile_function_args(md.syntaxes(), instrs, symtable, span_pos, span.args) - .map_err(|e| Error::from_call_error(md, e, span_pos))?; + let nargs = compile_function_args(md, instrs, symtable, span_pos, span.args)?; instrs.push(Instruction::FunctionCall(key, vtype, span_pos, nargs)); Ok(vtype) } - Some(SymbolPrototype::Variable(_)) => Err(Error::new( - span.vref_pos, - format!("{} is not an array nor a function", span.vref.name()), - )), + Some(SymbolPrototype::Variable(_)) => { + Err(Error::NotArrayOrFunction(span.vref_pos, key)) + } - None => Err(Error::new( - span.vref_pos, - format!("Unknown function or array {}", span.vref.name()), - )), + None => Err(Error::UndefinedSymbol(span.vref_pos, key)), } } } @@ -804,9 +734,9 @@ pub(super) fn compile_expr_as_type( Ok(()) } else { if target.is_numerical() { - Err(Error::new(epos, format!("{} is not a number", etype))) + Err(Error::NotANumber(epos, etype)) } else { - Err(Error::new(epos, format!("{} is not a {}", etype, target))) + Err(Error::TypeMismatch(epos, etype, target)) } } } @@ -881,7 +811,7 @@ mod tests { ) .parse("i = f") .compile() - .expect_err("1:5: In call to f: function requires arguments") + .expect_err("1:5: F expected i%") .check(); } @@ -902,7 +832,7 @@ mod tests { .define_callable(CallableMetadataBuilder::new("F").with_return_type(ExprType::Integer)) .parse("c f") .compile() - .expect_err("1:1: In call to C: 1:3: f is not an array nor a function") + .expect_err("1:3: F is not an array nor a function") .check(); } @@ -941,7 +871,7 @@ mod tests { )])) .parse("a = 3: c a$") .compile() - .expect_err("1:8: In call to C: 1:10: Incompatible type annotation in a$ reference") + .expect_err("1:10: Incompatible type annotation in a$ reference") .check(); } @@ -962,7 +892,7 @@ mod tests { .define_callable(CallableMetadataBuilder::new("F").with_return_type(ExprType::Integer)) .parse("c f$") .compile() - .expect_err("1:1: In call to C: 1:3: Incompatible type annotation in f$ reference") + .expect_err("1:3: Incompatible type annotation in f$ reference") .check(); } @@ -1007,7 +937,7 @@ mod tests { .define_callable(CallableMetadataBuilder::new("C")) .parse("b = c") .compile() - .expect_err("1:5: c is not an array nor a function") + .expect_err("1:5: C is not an array nor a function") .check(); } @@ -1027,7 +957,7 @@ mod tests { )])) .parse("c c") .compile() - .expect_err("1:1: In call to C: 1:3: c is not an array nor a function") + .expect_err("1:3: C is not an array nor a function") .check(); } @@ -1263,7 +1193,7 @@ mod tests { .define("FOO", SymbolPrototype::Array(ExprType::Integer, 1)) .parse("i = FOO(FALSE)") .compile() - .expect_err("1:9: Array index must be INTEGER, not BOOLEAN") + .expect_err("1:9: BOOLEAN is not a number") .check(); } @@ -1279,11 +1209,7 @@ mod tests { #[test] fn test_compile_expr_array_ref_not_defined() { - Tester::default() - .parse("i = a(4)") - .compile() - .expect_err("1:5: Unknown function or array a") - .check(); + Tester::default().parse("i = a(4)").compile().expect_err("1:5: Undefined symbol A").check(); } #[test] @@ -1292,7 +1218,7 @@ mod tests { .define("a", SymbolPrototype::Variable(ExprType::Integer)) .parse("i = a(3)") .compile() - .expect_err("1:5: a is not an array nor a function") + .expect_err("1:5: A is not an array nor a function") .check(); } @@ -1339,7 +1265,7 @@ mod tests { .define("k", SymbolPrototype::Variable(ExprType::Integer)) .parse("i = FOO(3, j, k + 1)") .compile() - .expect_err("1:5: Unknown function or array FOO") + .expect_err("1:5: Undefined symbol FOO") .check(); } @@ -1348,7 +1274,7 @@ mod tests { Tester::default() .parse("i = 3: j = i()") .compile() - .expect_err("1:12: i is not an array nor a function") + .expect_err("1:12: I is not an array nor a function") .check(); } } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 0c13525c..22053002 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -18,9 +18,7 @@ use crate::ast::*; use crate::bytecode::*; use crate::reader::LineCol; -use crate::syms::{ - CallError, CallableMetadata, CallableMetadataBuilder, Symbol, SymbolKey, Symbols, -}; +use crate::syms::{CallableMetadata, CallableMetadataBuilder, Symbol, SymbolKey, Symbols}; use std::borrow::Cow; use std::collections::HashMap; #[cfg(test)] @@ -33,48 +31,64 @@ use exprs::{compile_expr, compile_expr_as_type, compile_expr_in_command}; /// Compilation errors. #[derive(Debug, thiserror::Error)] -#[error("{}: {}", pos, message)] -pub struct Error { - pub(crate) pos: LineCol, - pub(crate) message: String, -} +#[allow(missing_docs)] // The error messages and names are good enough. +pub enum Error { + #[error("{0}: Cannot index array with {1} subscripts; need {2}")] + ArrayIndexSubscriptsError(LineCol, usize, usize), -impl Error { - /// Constructs a new compilation error from its parts. - pub(crate) fn new>(pos: LineCol, message: S) -> Self { - let message = message.into(); - Self { pos, message } - } - - /// Annotates a call evaluation error with the command's metadata. - // TODO(jmmv): This is a hack to support the transition to a parameterized arguments compilers - // in callables. Should be removed and/or somehow unified with the `Error` in this module given - // that `CallError` can specify errors that make no sense in this context. - fn from_call_error(md: &CallableMetadata, e: CallError, pos: LineCol) -> Self { - match e { - CallError::ArgumentError(pos2, e) => { - Self::new(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } - CallError::EvalError(pos2, e) => { - if !md.is_function() { - Self::new(pos2, e) - } else { - Self::new(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } - } - CallError::InternalError(pos2, e) => { - Self::new(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } - CallError::IoError(_) => unreachable!("I/O errors are not possible during compilation"), - CallError::NestedError(_) => unreachable!("Nested are not possible during compilation"), - CallError::SyntaxError if md.syntax().is_empty() => { - Self::new(pos, format!("In call to {}: expected no arguments", md.name())) - } - CallError::SyntaxError => { - Self::new(pos, format!("In call to {}: expected {}", md.name(), md.syntax())) - } - } - } + #[error("{0}: Cannot {1} {2} and {3}")] + BinaryOpTypeError(LineCol, &'static str, ExprType, ExprType), + + #[error("{0}: {} expected {}", .1.name(), .1.syntax())] + CallableSyntaxError(LineCol, CallableMetadata), + + #[error("{0}: Duplicate label {1}")] + DuplicateLabel(LineCol, String), + + #[error("{0}: Cannot assign value of type {1} to variable of type {2}")] + IncompatibleTypesInAssignment(LineCol, ExprType, ExprType), + + #[error("{0}: Incompatible type annotation in {1} reference")] + IncompatibleTypeAnnotationInReference(LineCol, VarRef), + + #[error("{0}: Cannot index non-array {1}")] + IndexNonArray(LineCol, String), + + #[error("{0}: EXIT DO outside of DO loop")] + MisplacedExitDo(LineCol), + + #[error("{0}: {1} requires a boolean condition")] + NotABooleanCondition(LineCol, String), + + #[error("{0}: {1} is not a command")] + NotACommand(LineCol, VarRef), + + #[error("{0}: {1} is not a number")] + NotANumber(LineCol, ExprType), + + #[error("{0}: Requires a reference, not a value")] + NotAReference(LineCol), + + #[error("{0}: {1} is not a variable")] + NotAVariable(LineCol, VarRef), + + #[error("{0}: {1} is not an array nor a function")] + NotArrayOrFunction(LineCol, SymbolKey), + + #[error("{0}: Cannot define already-defined symbol {1}")] + RedefinitionError(LineCol, SymbolKey), + + #[error("{0}: expected {2} but found {1}")] + TypeMismatch(LineCol, ExprType, ExprType), + + #[error("{0}: Cannot {1} {2}")] + UnaryOpTypeError(LineCol, &'static str, ExprType), + + #[error("{0}: Undefined symbol {1}")] + UndefinedSymbol(LineCol, SymbolKey), + + #[error("{0}: Unknown label {1}")] + UnknownLabel(LineCol, String), } /// Result for compiler return values. @@ -360,33 +374,21 @@ impl Compiler { let (atype, dims) = match self.symtable.get(&key) { Some(SymbolPrototype::Array(atype, dims)) => (*atype, *dims), Some(_) => { - return Err(Error::new( - span.vref_pos, - format!("Cannot index non-array {}", span.vref), - )); + return Err(Error::IndexNonArray(span.vref_pos, span.vref.take_name())); } None => { - return Err(Error::new( - span.vref_pos, - format!("Cannot index undefined array {}", span.vref), - )); + return Err(Error::UndefinedSymbol(span.vref_pos, key)); } }; if !span.vref.accepts(atype) { - return Err(Error::new( - span.vref_pos, - format!("Incompatible types in {} reference", span.vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(span.vref_pos, span.vref)); } let etype = self.compile_expr(span.expr, false)?; let etype = self.maybe_cast(atype, etype); if etype != atype { - return Err(Error::new( - span.vref_pos, - format!("Cannot assign value of type {} to array of type {}", etype, atype), - )); + return Err(Error::IncompatibleTypesInAssignment(span.vref_pos, etype, atype)); } let nargs = span.subscripts.len(); @@ -413,9 +415,7 @@ impl Compiler { let vtype = match self.symtable.get(&key) { Some(SymbolPrototype::Variable(vtype)) => *vtype, - Some(_) => { - return Err(Error::new(vref_pos, format!("Cannot redefine {} as a variable", vref))) - } + Some(_) => return Err(Error::RedefinitionError(vref_pos, key)), None => { // TODO(jmmv): Compile separate Dim instructions for new variables instead of // checking this every time. @@ -428,18 +428,12 @@ impl Compiler { let etype = self.maybe_cast(vtype, etype); if etype != vtype { - return Err(Error::new( - vref_pos, - format!("Cannot assign value of type {} to variable of type {}", etype, vtype,), - )); + return Err(Error::IncompatibleTypesInAssignment(vref_pos, etype, vtype)); } if let Some(ref_type) = vref.ref_type() { if ref_type != vtype { - return Err(Error::new( - vref_pos, - format!("Incompatible types in {} reference", vref), - )); + return Err(Error::IncompatibleTypeAnnotationInReference(vref_pos, vref)); } } @@ -452,10 +446,7 @@ impl Compiler { fn compile_callable(&mut self, span: CallableSpan) -> Result<()> { let key = SymbolKey::from(span.name.name()); if self.symtable.contains_key(&key) { - return Err(Error::new( - span.name_pos, - format!("Cannot define already-defined symbol {}", span.name), - )); + return Err(Error::RedefinitionError(span.name_pos, key)); } let mut syntax = vec![]; @@ -491,10 +482,7 @@ impl Compiler { fn compile_dim(&mut self, span: DimSpan) -> Result<()> { let key = SymbolKey::from(&span.name); if self.symtable.contains_key(&key) { - return Err(Error::new( - span.name_pos, - format!("Cannot DIM already-defined symbol {}", span.name), - )); + return Err(Error::RedefinitionError(span.name_pos, key)); } self.emit(Instruction::Dim(DimISpan { @@ -525,7 +513,7 @@ impl Compiler { DoGuard::PreUntil(guard) => { let start_pc = self.next_pc; - self.compile_expr_guard(guard, "DO requires a boolean condition")?; + self.compile_expr_guard(guard, "DO")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); @@ -534,7 +522,7 @@ impl Compiler { DoGuard::PreWhile(guard) => { let start_pc = self.next_pc; - self.compile_expr_guard(guard, "DO requires a boolean condition")?; + self.compile_expr_guard(guard, "DO")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); @@ -544,14 +532,14 @@ impl Compiler { DoGuard::PostUntil(guard) => { let start_pc = self.next_pc; self.compile_many(span.body)?; - self.compile_expr_guard(guard, "LOOP requires a boolean condition")?; + self.compile_expr_guard(guard, "LOOP")?; end_pc = self.emit(Instruction::JumpIfNotTrue(start_pc)); } DoGuard::PostWhile(guard) => { let start_pc = self.next_pc; self.compile_many(span.body)?; - self.compile_expr_guard(guard, "LOOP requires a boolean condition")?; + self.compile_expr_guard(guard, "LOOP")?; end_pc = self.emit(Instruction::JumpIfTrue(start_pc)); } } @@ -624,7 +612,7 @@ impl Compiler { while let Some(branch) = next { let next2 = iter.next(); - self.compile_expr_guard(branch.guard, "IF/ELSEIF require a boolean condition")?; + self.compile_expr_guard(branch.guard, "IF/ELSEIF")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(branch.body)?; @@ -731,7 +719,7 @@ impl Compiler { self.compile_many(case.body)?; } Some(guard) => { - self.compile_expr_guard(guard, "SELECT requires a boolean condition")?; + self.compile_expr_guard(guard, "SELECT")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(case.body)?; @@ -761,7 +749,7 @@ impl Compiler { /// Compiles a `WHILE` loop and appends its instructions to the compilation context. fn compile_while(&mut self, span: WhileSpan) -> Result<()> { let start_pc = self.next_pc; - self.compile_expr_guard(span.expr, "WHILE requires a boolean condition")?; + self.compile_expr_guard(span.expr, "WHILE")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; @@ -809,7 +797,7 @@ impl Compiler { let pos = guard.start_pos(); match self.compile_expr(guard, false)? { ExprType::Boolean => Ok(()), - _ => Err(Error::new(pos, errmsg.into())), + _ => Err(Error::NotABooleanCondition(pos, errmsg.into())), } } @@ -829,38 +817,26 @@ impl Compiler { let md = match self.symtable.get(&key) { Some(SymbolPrototype::Callable(md)) => { if md.is_function() { - return Err(Error::new( - span.vref_pos, - format!("{} is not a command", span.vref.name()), - )); + return Err(Error::NotACommand(span.vref_pos, span.vref)); } md.clone() } Some(_) => { - return Err(Error::new( - span.vref_pos, - format!("{} is not a command", span.vref.name()), - )); + return Err(Error::NotACommand(span.vref_pos, span.vref)); } - None => { - return Err(Error::new( - span.vref_pos, - format!("Unknown builtin {}", span.vref.name()), - )) - } + None => return Err(Error::UndefinedSymbol(span.vref_pos, key)), }; let name_pos = span.vref_pos; let nargs = compile_command_args( - md.syntaxes(), + &md, &mut self.instrs, &mut self.symtable, name_pos, span.args, - ) - .map_err(|e| Error::from_call_error(&md, e, name_pos))?; + )?; self.next_pc = self.instrs.len(); self.emit(Instruction::BuiltinCall(key, span.vref_pos, nargs)); } @@ -880,10 +856,7 @@ impl Compiler { Statement::DimArray(span) => { let key = SymbolKey::from(&span.name); if self.symtable.contains_key(&key) { - return Err(Error::new( - span.name_pos, - format!("Cannot DIM already-defined symbol {}", span.name), - )); + return Err(Error::RedefinitionError(span.name_pos, key)); } let nargs = span.dimensions.len(); @@ -922,7 +895,7 @@ impl Compiler { Statement::ExitDo(span) => { if self.exit_do_level.1 == 0 { - return Err(Error::new(span.pos, "EXIT DO outside of DO loop".to_owned())); + return Err(Error::MisplacedExitDo(span.pos)); } let exit_do_pc = self.emit(Instruction::Nop); self.fixups.insert( @@ -951,10 +924,7 @@ impl Compiler { Statement::Label(span) => { if self.labels.insert(span.name.clone(), self.next_pc).is_some() { - return Err(Error::new( - span.name_pos, - format!("Duplicate label {}", span.name), - )); + return Err(Error::DuplicateLabel(span.name_pos, span.name)); } } @@ -1095,10 +1065,7 @@ impl Compiler { let addr = match self.labels.get(&fixup.target) { Some(addr) => *addr, None => { - return Err(Error::new( - fixup.target_pos, - format!("Unknown label {}", fixup.target), - )); + return Err(Error::UnknownLabel(fixup.target_pos, fixup.target)); } }; @@ -1344,7 +1311,7 @@ mod tests { .define("a", SymbolPrototype::Array(ExprType::Integer, 1)) .parse("a(TRUE) = 1") .compile() - .expect_err("1:3: Array index must be INTEGER, not BOOLEAN") + .expect_err("1:3: BOOLEAN is not a number") .check(); } @@ -1354,7 +1321,7 @@ mod tests { .define("a", SymbolPrototype::Array(ExprType::Integer, 1)) .parse("a#(0) = 1") .compile() - .expect_err("1:1: Incompatible types in a# reference") + .expect_err("1:1: Incompatible type annotation in a# reference") .check(); } @@ -1393,7 +1360,7 @@ mod tests { .define("i", SymbolPrototype::Variable(ExprType::Integer)) .parse("a(3) = FALSE") .compile() - .expect_err("1:1: Cannot assign value of type BOOLEAN to array of type DOUBLE") + .expect_err("1:1: Cannot assign value of type BOOLEAN to variable of type DOUBLE") .check(); } @@ -1413,7 +1380,7 @@ mod tests { Tester::default() .parse("a(3) = FALSE") .compile() - .expect_err("1:1: Cannot index undefined array a") + .expect_err("1:1: Undefined symbol A") .check(); } @@ -1486,7 +1453,7 @@ mod tests { .define(SymbolKey::from("foo"), SymbolPrototype::Variable(ExprType::Text)) .parse("foo# = \"hello\"") .compile() - .expect_err("1:1: Incompatible types in foo# reference") + .expect_err("1:1: Incompatible type annotation in foo# reference") .check(); } @@ -1607,7 +1574,7 @@ mod tests { Tester::default() .parse("DIM foo: DIM Foo") .compile() - .expect_err("1:14: Cannot DIM already-defined symbol Foo") + .expect_err("1:14: Cannot define already-defined symbol FOO") .check(); } @@ -1617,7 +1584,7 @@ mod tests { .define("SomeArray", SymbolPrototype::Array(ExprType::Integer, 3)) .parse("DIM somearray") .compile() - .expect_err("1:5: Cannot DIM already-defined symbol somearray") + .expect_err("1:5: Cannot define already-defined symbol SOMEARRAY") .check(); Tester::default() @@ -1626,14 +1593,14 @@ mod tests { ) .parse("DIM OuT") .compile() - .expect_err("1:5: Cannot DIM already-defined symbol OuT") + .expect_err("1:5: Cannot define already-defined symbol OUT") .check(); Tester::default() .define("SomeVar", SymbolPrototype::Variable(ExprType::Integer)) .parse("DIM SOMEVAR") .compile() - .expect_err("1:5: Cannot DIM already-defined symbol SOMEVAR") + .expect_err("1:5: Cannot define already-defined symbol SOMEVAR") .check(); } @@ -1658,13 +1625,13 @@ mod tests { Tester::default() .parse("DIM var AS BOOLEAN: DIM SHARED Var AS BOOLEAN") .compile() - .expect_err("1:32: Cannot DIM already-defined symbol Var") + .expect_err("1:32: Cannot define already-defined symbol VAR") .check(); Tester::default() .parse("DIM SHARED var AS BOOLEAN: DIM Var AS BOOLEAN") .compile() - .expect_err("1:32: Cannot DIM already-defined symbol Var") + .expect_err("1:32: Cannot define already-defined symbol VAR") .check(); } @@ -1767,13 +1734,13 @@ mod tests { Tester::default() .parse("DIM var(1) AS BOOLEAN: DIM SHARED Var(1) AS BOOLEAN") .compile() - .expect_err("1:35: Cannot DIM already-defined symbol Var") + .expect_err("1:35: Cannot define already-defined symbol VAR") .check(); Tester::default() .parse("DIM SHARED var(1) AS BOOLEAN: DIM Var(1) AS BOOLEAN") .compile() - .expect_err("1:35: Cannot DIM already-defined symbol Var") + .expect_err("1:35: Cannot define already-defined symbol VAR") .check(); } @@ -2127,7 +2094,7 @@ mod tests { Tester::default() .parse("a = 1: FUNCTION a: END FUNCTION") .compile() - .expect_err("1:17: Cannot define already-defined symbol a%") + .expect_err("1:17: Cannot define already-defined symbol A") .check(); } @@ -2136,7 +2103,7 @@ mod tests { Tester::default() .parse("FUNCTION a: END FUNCTION: FUNCTION A: END FUNCTION") .compile() - .expect_err("1:36: Cannot define already-defined symbol A%") + .expect_err("1:36: Cannot define already-defined symbol A") .check(); } @@ -2145,7 +2112,7 @@ mod tests { Tester::default() .parse("SUB a: END SUB: FUNCTION A: END FUNCTION") .compile() - .expect_err("1:26: Cannot define already-defined symbol A%") + .expect_err("1:26: Cannot define already-defined symbol A") .check(); } @@ -2154,7 +2121,7 @@ mod tests { Tester::default() .parse("a = 1: SUB a: END SUB") .compile() - .expect_err("1:12: Cannot define already-defined symbol a") + .expect_err("1:12: Cannot define already-defined symbol A") .check(); } diff --git a/core/src/exec.rs b/core/src/exec.rs index 851274c9..8e039037 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1004,7 +1004,7 @@ impl Machine { match self.symbols.load(key) { Some(Symbol::Variable(v)) => Ok(v), Some(_) => unreachable!("Variable type checking has been done at compile time"), - None => new_syntax_error(pos, format!("Undefined variable {}", key)), + None => new_syntax_error(pos, format!("Undefined symbol {}", key)), } } @@ -1757,7 +1757,7 @@ mod tests { } match machine.get_symbols().get_auto("b") { Some(Symbol::Variable(Value::Integer(_))) => (), - e => panic!("a is not an integer: {:?}", e), + e => panic!("A is not an integer: {:?}", e), } assert!(!*cleared.borrow()); machine.clear(); @@ -1912,18 +1912,18 @@ mod tests { #[test] fn test_array_assignment_errors() { - do_simple_error_test("a() = 3\n", "1:1: Cannot index undefined array a"); + do_simple_error_test("a() = 3\n", "1:1: Undefined symbol A"); do_simple_error_test("a = 3\na(0) = 3\n", "2:1: Cannot index non-array a"); do_simple_error_test( "DIM a(2)\na() = 3\n", "2:1: Cannot index array with 0 subscripts; need 1", ); do_simple_error_test("DIM a(1)\na(-1) = 3\n", "2:1: Subscript -1 cannot be negative"); + do_simple_error_test("DIM a(1, 2)\na(1, TRUE) = 3\n", "2:6: BOOLEAN is not a number"); do_simple_error_test( - "DIM a(1, 2)\na(1, TRUE) = 3\n", - "2:6: Array index must be INTEGER, not BOOLEAN", + "DIM a(2)\na$(1) = 3", + "2:1: Incompatible type annotation in a$ reference", ); - do_simple_error_test("DIM a(2)\na$(1) = 3", "2:1: Incompatible types in a$ reference"); } #[test] @@ -1976,7 +1976,7 @@ mod tests { #[test] fn test_assignment_errors() { do_simple_error_test("a =\n", "1:4: Missing expression in assignment"); - do_simple_error_test("a = b\n", "1:5: Undefined variable b"); + do_simple_error_test("a = b\n", "1:5: Undefined symbol B"); do_simple_error_test( "a = 3\na = TRUE\n", "2:1: Cannot assign value of type BOOLEAN to variable of type INTEGER", @@ -1994,9 +1994,9 @@ mod tests { #[test] fn test_dim_errors() { - do_simple_error_test("DIM i\nDIM i", "2:5: Cannot DIM already-defined symbol i"); - do_simple_error_test("DIM i\nDIM I", "2:5: Cannot DIM already-defined symbol I"); - do_simple_error_test("i = 0\nDIM i", "2:5: Cannot DIM already-defined symbol i"); + do_simple_error_test("DIM i\nDIM i", "2:5: Cannot define already-defined symbol I"); + do_simple_error_test("DIM i\nDIM I", "2:5: Cannot define already-defined symbol I"); + do_simple_error_test("i = 0\nDIM i", "2:5: Cannot define already-defined symbol I"); } #[test] @@ -2018,7 +2018,7 @@ mod tests { do_simple_error_test("DIM i()", "1:6: Arrays require at least one dimension"); do_simple_error_test("DIM i(FALSE)", "1:7: BOOLEAN is not a number"); do_simple_error_test("DIM i(-3)", "1:7: Dimensions in DIM array must be positive"); - do_simple_error_test("DIM i\nDIM i(3)", "2:5: Cannot DIM already-defined symbol i"); + do_simple_error_test("DIM i\nDIM i(3)", "2:5: Cannot define already-defined symbol I"); } #[test] @@ -2401,7 +2401,7 @@ mod tests { "#, &[], &["running"], - "6:17: Undefined variable A", + "6:17: Undefined symbol A", ) } @@ -2464,7 +2464,7 @@ mod tests { OUT "no match" END IF "#; - do_error_test(code, &["5"], &[], "5:20: IF/ELSEIF require a boolean condition"); + do_error_test(code, &["5"], &[], "5:20: IF/ELSEIF requires a boolean condition"); } #[test] @@ -2477,10 +2477,10 @@ mod tests { do_simple_error_test("IF TRUE\nEND IF\nOUT 3", "1:8: No THEN in IF statement"); do_simple_error_test("IF 2\nEND IF", "1:5: No THEN in IF statement"); - do_simple_error_test("IF 2 THEN\nEND IF", "1:4: IF/ELSEIF require a boolean condition"); + do_simple_error_test("IF 2 THEN\nEND IF", "1:4: IF/ELSEIF requires a boolean condition"); do_simple_error_test( "IF FALSE THEN\nELSEIF 2 THEN\nEND IF", - "2:8: IF/ELSEIF require a boolean condition", + "2:8: IF/ELSEIF requires a boolean condition", ); } @@ -2579,22 +2579,16 @@ mod tests { do_simple_error_test("FOR\nNEXT", "1:4: No iterator name in FOR statement"); do_simple_error_test("FOR a = 1 TO 10\nEND IF", "2:1: END IF without IF"); - do_simple_error_test( - "FOR i = \"a\" TO 3\nNEXT", - "1:13: Cannot compare STRING and INTEGER with <=", - ); - do_simple_error_test( - "FOR i = 1 TO \"a\"\nNEXT", - "1:11: Cannot compare INTEGER and STRING with <=", - ); + do_simple_error_test("FOR i = \"a\" TO 3\nNEXT", "1:13: Cannot <= STRING and INTEGER"); + do_simple_error_test("FOR i = 1 TO \"a\"\nNEXT", "1:11: Cannot <= INTEGER and STRING"); do_simple_error_test( "FOR i = \"b\" TO 7 STEP -8\nNEXT", - "1:13: Cannot compare STRING and INTEGER with >=", + "1:13: Cannot >= STRING and INTEGER", ); do_simple_error_test( "FOR i = 1 TO \"b\" STEP -8\nNEXT", - "1:11: Cannot compare INTEGER and STRING with >=", + "1:11: Cannot >= INTEGER and STRING", ); } @@ -2617,10 +2611,7 @@ mod tests { do_simple_error_test("OUT OUT()", "1:5: OUT is not an array nor a function"); do_simple_error_test("OUT OUT(3)", "1:5: OUT is not an array nor a function"); do_simple_error_test("OUT SUM?()", "1:5: Incompatible type annotation in SUM? reference"); - do_simple_error_test( - "OUT TYPE_CHECK()", - "1:5: In call to TYPE_CHECK: expected no arguments nor parenthesis", - ); + do_simple_error_test("OUT TYPE_CHECK()", "1:5: TYPE_CHECK expected no arguments"); } #[test] @@ -3123,7 +3114,7 @@ mod tests { do_simple_error_test( "SELECT CASE 2\nCASE FALSE\nEND SELECT", - "2:6: Cannot compare INTEGER and BOOLEAN with =", + "2:6: Cannot = INTEGER and BOOLEAN", ); } @@ -3198,8 +3189,8 @@ mod tests { #[test] fn test_top_level_compilation_errors_abort_execution() { - do_simple_error_test("FOO BAR", "1:1: Unknown builtin FOO"); - do_error_test(r#"OUT "a": FOO BAR: OUT "b""#, &[], &[], "1:10: Unknown builtin FOO"); + do_simple_error_test("FOO BAR", "1:1: Undefined symbol FOO"); + do_error_test(r#"OUT "a": FOO BAR: OUT "b""#, &[], &[], "1:10: Undefined symbol FOO"); } #[test] @@ -3218,12 +3209,12 @@ mod tests { #[test] fn test_inner_level_compilation_errors_abort_execution() { - do_simple_error_test(r#"IF TRUE THEN: FOO BAR: END IF"#, "1:15: Unknown builtin FOO"); + do_simple_error_test(r#"IF TRUE THEN: FOO BAR: END IF"#, "1:15: Undefined symbol FOO"); do_error_test( r#"OUT "a": IF TRUE THEN: FOO BAR: END IF: OUT "b""#, &[], &[], - "1:24: Unknown builtin FOO", + "1:24: Undefined symbol FOO", ); } @@ -3404,7 +3395,7 @@ mod tests { END FUNCTION OUT foo(3, 4) "#; - do_error_test(code, &[], &[], "5:17: In call to FOO: expected n%"); + do_error_test(code, &[], &[], "5:17: FOO expected n%"); } #[test] @@ -3452,7 +3443,7 @@ mod tests { SUB f: OUT "foo": END SUB OUT f "#; - do_error_test(code, &[], &[], "3:17: f is not an array nor a function"); + do_error_test(code, &[], &[], "3:17: F is not an array nor a function"); } #[test] @@ -3463,6 +3454,6 @@ mod tests { END SUB foo 3, 4 "#; - do_error_test(code, &[], &[], "5:13: In call to FOO: expected n%"); + do_error_test(code, &[], &[], "5:13: FOO expected n%"); } } diff --git a/core/src/syms.rs b/core/src/syms.rs index ac0b675f..2485efc4 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -65,7 +65,7 @@ impl From for CallError { impl From for CallError { fn from(value: compiler::Error) -> Self { - Self::EvalError(value.pos, value.message) + Self::NestedError(exec::Error::CompilerError(value)) } } @@ -403,7 +403,10 @@ impl Symbols { if let Some(symbol) = symbol { let stype = symbol.eval_type(); if !vref.accepts_callable(stype) { - return Err(Error::new(format!("Incompatible types in {} reference", vref))); + return Err(Error::new(format!( + "Incompatible type annotation in {} reference", + vref + ))); } } Ok(symbol) @@ -423,7 +426,10 @@ impl Symbols { Some(symbol) => { let stype = symbol.eval_type(); if !vref.accepts_callable(stype) { - return Err(Error::new(format!("Incompatible types in {} reference", vref))); + return Err(Error::new(format!( + "Incompatible type annotation in {} reference", + vref + ))); } Ok(Some(symbol)) } @@ -525,7 +531,6 @@ pub struct CallableMetadataBuilder { name: Cow<'static, str>, return_type: Option, category: Option<&'static str>, - syntax: Option, syntaxes: Vec, description: Option<&'static str>, } @@ -542,7 +547,6 @@ impl CallableMetadataBuilder { Self { name: Cow::Borrowed(name), return_type: None, - syntax: None, syntaxes: vec![], category: None, description: None, @@ -557,7 +561,6 @@ impl CallableMetadataBuilder { Self { name: Cow::Owned(name.to_ascii_uppercase()), return_type: None, - syntax: None, syntaxes: vec![], category: None, description: None, @@ -575,51 +578,29 @@ impl CallableMetadataBuilder { mut self, syntaxes: &'static [(&'static [SingularArgSyntax], Option<&'static RepeatedSyntax>)], ) -> Self { - let syntaxes = syntaxes + self.syntaxes = syntaxes .iter() .map(|s| CallableSyntax::new_static(s.0, s.1)) .collect::>(); - if syntaxes.is_empty() { - self.syntax = Some("no arguments".to_owned()); - } else if syntaxes.len() == 1 { - self.syntax = Some(syntaxes.first().unwrap().describe()); - } else { - self.syntax = Some( - syntaxes - .iter() - .map(|syn| format!("<{}>", syn.describe())) - .collect::>() - .join(" | "), - ); - }; - self.syntaxes = syntaxes; self } /// Sets the syntax specifications for this callable. - pub fn with_dynamic_syntax( - mut self, + 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::>(); - if syntaxes.is_empty() { - self.syntax = Some("no arguments".to_owned()); - } else if syntaxes.len() == 1 { - self.syntax = Some(syntaxes.first().unwrap().describe()); - } else { - self.syntax = Some( - syntaxes - .iter() - .map(|syn| format!("<{}>", syn.describe())) - .collect::>() - .join(" | "), - ); - }; - self.syntaxes = syntaxes; - self + self.with_syntaxes(syntaxes) } /// Sets the category for this callable. All callables with the same category name will be @@ -647,7 +628,6 @@ impl CallableMetadataBuilder { CallableMetadata { name: self.name, return_type: self.return_type, - syntax: self.syntax.expect("All callables must specify a syntax"), syntaxes: self.syntaxes, category: self.category.expect("All callables must specify a category"), description: self.description.expect("All callables must specify a description"), @@ -663,7 +643,6 @@ impl CallableMetadataBuilder { CallableMetadata { name: self.name, return_type: self.return_type, - syntax: self.syntax.unwrap_or("".to_owned()), syntaxes: self.syntaxes, category: self.category.unwrap_or(""), description: self.description.unwrap_or(""), @@ -679,7 +658,6 @@ impl CallableMetadataBuilder { pub struct CallableMetadata { name: Cow<'static, str>, return_type: Option, - syntax: String, syntaxes: Vec, category: &'static str, description: &'static str, @@ -697,8 +675,24 @@ impl CallableMetadata { } /// Gets the callable's syntax specification. - pub fn syntax(&self) -> &str { - &self.syntax + 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. @@ -720,7 +714,7 @@ impl CallableMetadata { /// Returns true if this is a callable that takes no arguments. pub fn is_argless(&self) -> bool { - self.syntax.is_empty() + self.syntaxes.is_empty() || (self.syntaxes.len() == 1 && self.syntaxes[0].is_empty()) } /// Returns true if this callable is a function (not a command). @@ -1011,7 +1005,7 @@ mod tests { } } assert_eq!( - "Incompatible types in bool_array$ reference", + "Incompatible type annotation in bool_array$ reference", format!("{}", syms.get(&VarRef::new("bool_array", Some(ExprType::Text))).unwrap_err()) ); @@ -1020,7 +1014,7 @@ mod tests { _ => panic!("Got something that is not the command we asked for"), } assert_eq!( - "Incompatible types in out# reference", + "Incompatible type annotation in out# reference", format!("{}", syms.get(&VarRef::new("out", Some(ExprType::Double))).unwrap_err()) ); @@ -1033,7 +1027,7 @@ mod tests { } } assert_eq!( - "Incompatible types in sum# reference", + "Incompatible type annotation in sum# reference", format!("{}", syms.get(&VarRef::new("sum", Some(ExprType::Double))).unwrap_err()) ); @@ -1044,7 +1038,7 @@ mod tests { } } assert_eq!( - "Incompatible types in string_var% reference", + "Incompatible type annotation in string_var% reference", format!( "{}", syms.get(&VarRef::new("string_var", Some(ExprType::Integer))).unwrap_err() @@ -1099,7 +1093,7 @@ mod tests { } } assert_eq!( - "Incompatible types in bool_array$ reference", + "Incompatible type annotation in bool_array$ reference", format!( "{}", syms.get_mut(&VarRef::new("bool_array", Some(ExprType::Text))).unwrap_err() @@ -1111,7 +1105,7 @@ mod tests { _ => panic!("Got something that is not the command we asked for"), } assert_eq!( - "Incompatible types in out# reference", + "Incompatible type annotation in out# reference", format!("{}", syms.get_mut(&VarRef::new("out", Some(ExprType::Double))).unwrap_err()) ); @@ -1124,7 +1118,7 @@ mod tests { } } assert_eq!( - "Incompatible types in sum# reference", + "Incompatible type annotation in sum# reference", format!("{}", syms.get_mut(&VarRef::new("sum", Some(ExprType::Double))).unwrap_err()) ); @@ -1135,7 +1129,7 @@ mod tests { } } assert_eq!( - "Incompatible types in string_var% reference", + "Incompatible type annotation in string_var% reference", format!( "{}", syms.get_mut(&VarRef::new("string_var", Some(ExprType::Integer))).unwrap_err() @@ -1409,7 +1403,7 @@ mod tests { ); assert_eq!( - "Incompatible types in SomeArray$ reference", + "Incompatible type annotation in SomeArray$ reference", format!( "{}", syms.set_var(&VarRef::new("SomeArray", Some(ExprType::Text)), Value::Integer(1)) diff --git a/repl/src/lib.rs b/repl/src/lib.rs index ce63329e..0fa257f3 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -306,7 +306,7 @@ mod tests { .expect_var("after", 5) .expect_prints([ "Loading AUTOEXEC.BAS...", - "AUTOEXEC.BAS failed: 2:5: Undefined variable undef", + "AUTOEXEC.BAS failed: 2:5: Undefined symbol UNDEF", ]) .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec) .check(); diff --git a/std/src/arrays.rs b/std/src/arrays.rs index 82d989ea..0ee1c8fc 100644 --- a/std/src/arrays.rs +++ b/std/src/arrays.rs @@ -237,7 +237,7 @@ mod tests { Tester::default() .run(format!("DIM x(2): result = {}()", func)) .expect_compilation_err(format!( - "1:20: In call to {}: expected | ", + "1:20: {} expected | ", func )) .check(); @@ -245,7 +245,7 @@ mod tests { Tester::default() .run(format!("DIM x(2): result = {}(x, 1, 2)", func)) .expect_compilation_err(format!( - "1:20: In call to {}: expected | ", + "1:20: {} expected | ", func )) .check(); @@ -258,47 +258,32 @@ mod tests { Tester::default() .run(format!("DIM x(2): result = {}(x, TRUE)", func)) - .expect_compilation_err(format!( - "1:20: In call to {}: 1:30: BOOLEAN is not a number", - func - )) + .expect_compilation_err("1:30: BOOLEAN is not a number") .check(); Tester::default() .run(format!("i = 0: result = {}(i)", func)) - .expect_compilation_err(format!( - "1:17: In call to {}: 1:24: i is not an array reference", - func - )) + .expect_compilation_err("1:24: Requires a reference, not a value") .check(); Tester::default() .run(format!("result = {}(3)", func)) - .expect_compilation_err(format!( - "1:10: In call to {}: 1:17: Requires an array reference, not a value", - func - )) + .expect_compilation_err("1:17: Requires a reference, not a value") .check(); Tester::default() .run(format!("i = 0: result = {}(i)", func)) - .expect_compilation_err(format!( - "1:17: In call to {}: 1:24: i is not an array reference", - func - )) + .expect_compilation_err("1:24: Requires a reference, not a value") .check(); Tester::default() .run(format!("DIM i(3) AS BOOLEAN: result = {}(i$)", func)) - .expect_compilation_err(format!( - "1:31: In call to {}: 1:38: Incompatible type annotation in i$ reference", - func - )) + .expect_compilation_err("1:38: Incompatible type annotation in i$ reference") .check(); Tester::default() .run(format!("result = {}(x)", func)) - .expect_compilation_err(format!("1:10: In call to {}: 1:17: Undefined array x", func)) + .expect_compilation_err("1:17: Undefined symbol X") .check(); Tester::default() diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index 7ca0aa0d..b951f82d 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -695,7 +695,7 @@ mod tests { #[test] fn test_cls_errors() { - check_stmt_compilation_err("1:1: In call to CLS: expected no arguments", "CLS 1"); + check_stmt_compilation_err("1:1: CLS expected no arguments", "CLS 1"); } #[test] @@ -718,25 +718,16 @@ mod tests { #[test] fn test_color_errors() { check_stmt_compilation_err( - "1:1: In call to COLOR: expected <> | | <[fg%], [bg%]>", + "1:1: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1, 2, 3", ); - check_stmt_compilation_err( - "1:1: In call to COLOR: expected <> | | <[fg%], [bg%]>", - "COLOR 1; 2", - ); + check_stmt_compilation_err("1:1: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1; 2"); check_stmt_err("1:1: In call to COLOR: 1:7: Color out of range", "COLOR 1000, 0"); check_stmt_err("1:1: In call to COLOR: 1:10: Color out of range", "COLOR 0, 1000"); - check_stmt_compilation_err( - "1:1: In call to COLOR: 1:7: BOOLEAN is not a number", - "COLOR TRUE, 0", - ); - check_stmt_compilation_err( - "1:1: In call to COLOR: 1:10: BOOLEAN is not a number", - "COLOR 0, TRUE", - ); + check_stmt_compilation_err("1:7: BOOLEAN is not a number", "COLOR TRUE, 0"); + check_stmt_compilation_err("1:10: BOOLEAN is not a number", "COLOR 0, TRUE"); } #[test] @@ -763,14 +754,8 @@ mod tests { #[test] fn test_inkey_errors() { - check_expr_compilation_error( - "1:10: In call to INKEY: expected no arguments nor parenthesis", - "INKEY()", - ); - check_expr_compilation_error( - "1:10: In call to INKEY: expected no arguments nor parenthesis", - "INKEY(1)", - ); + check_expr_compilation_error("1:10: INKEY expected no arguments", "INKEY()"); + check_expr_compilation_error("1:10: INKEY expected no arguments", "INKEY(1)"); } #[test] @@ -861,37 +846,26 @@ mod tests { #[test] fn test_input_errors() { + check_stmt_compilation_err("1:1: INPUT expected | <[prompt$] <,|;> vref>", "INPUT"); check_stmt_compilation_err( - "1:1: In call to INPUT: expected | <[prompt$] <,|;> vref>", - "INPUT", - ); - check_stmt_compilation_err( - "1:1: In call to INPUT: expected | <[prompt$] <,|;> vref>", + "1:1: INPUT expected | <[prompt$] <,|;> vref>", "INPUT ; ,", ); check_stmt_compilation_err( - "1:1: In call to INPUT: expected | <[prompt$] <,|;> vref>", + "1:1: INPUT expected | <[prompt$] <,|;> vref>", "INPUT ;", ); + check_stmt_compilation_err("1:7: expected STRING but found INTEGER", "INPUT 3 ; a"); check_stmt_compilation_err( - "1:1: In call to INPUT: 1:7: INTEGER is not a STRING", - "INPUT 3 ; a", - ); - check_stmt_compilation_err( - "1:1: In call to INPUT: expected | <[prompt$] <,|;> vref>", + "1:1: INPUT expected | <[prompt$] <,|;> vref>", "INPUT \"foo\" AS bar", ); - check_stmt_err("1:1: In call to INPUT: 1:7: Undefined variable a", "INPUT a + 1 ; b"); + check_stmt_err("1:7: Undefined symbol A", "INPUT a + 1 ; b"); Tester::default() .run("a = 3: INPUT ; a + 1") - .expect_compilation_err( - "1:8: In call to INPUT: 1:16: Requires a variable reference, not a value", - ) + .expect_compilation_err("1:16: Requires a reference, not a value") .check(); - check_stmt_err( - "1:1: In call to INPUT: 1:11: Cannot + STRING and BOOLEAN", - "INPUT \"a\" + TRUE; b?", - ); + check_stmt_err("1:11: Cannot + STRING and BOOLEAN", "INPUT \"a\" + TRUE; b?"); } #[test] @@ -909,29 +883,20 @@ mod tests { #[test] fn test_locate_errors() { - check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE"); - check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE 1"); - check_stmt_compilation_err( - "1:1: In call to LOCATE: expected column%, row%", - "LOCATE 1, 2, 3", - ); - check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE 1; 2"); + check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE"); + check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1"); + check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1, 2, 3"); + check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1; 2"); check_stmt_err("1:1: In call to LOCATE: 1:8: Column out of range", "LOCATE -1, 2"); check_stmt_err("1:1: In call to LOCATE: 1:8: Column out of range", "LOCATE 70000, 2"); - check_stmt_compilation_err( - "1:1: In call to LOCATE: 1:8: BOOLEAN is not a number", - "LOCATE TRUE, 2", - ); - check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE , 2"); + check_stmt_compilation_err("1:8: BOOLEAN is not a number", "LOCATE TRUE, 2"); + check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE , 2"); check_stmt_err("1:1: In call to LOCATE: 1:11: Row out of range", "LOCATE 1, -2"); check_stmt_err("1:1: In call to LOCATE: 1:11: Row out of range", "LOCATE 1, 70000"); - check_stmt_compilation_err( - "1:1: In call to LOCATE: 1:11: BOOLEAN is not a number", - "LOCATE 1, TRUE", - ); - check_stmt_compilation_err("1:1: In call to LOCATE: expected column%, row%", "LOCATE 1,"); + check_stmt_compilation_err("1:11: BOOLEAN is not a number", "LOCATE 1, TRUE"); + check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1,"); let mut t = Tester::default(); t.get_console().borrow_mut().set_size_chars(CharsXY { x: 30, y: 20 }); @@ -1039,11 +1004,11 @@ mod tests { #[test] fn test_print_errors() { check_stmt_compilation_err( - "1:1: In call to PRINT: expected [expr1 <,|;> .. <,|;> exprN]", + "1:1: PRINT expected [expr1 <,|;> .. <,|;> exprN]", "PRINT 3 AS 4", ); check_stmt_compilation_err( - "1:1: In call to PRINT: expected [expr1 <,|;> .. <,|;> exprN]", + "1:1: PRINT expected [expr1 <,|;> .. <,|;> exprN]", "PRINT 3, 4 AS 5", ); // Ensure type errors from `Expr` and `Value` bubble up. @@ -1057,14 +1022,8 @@ mod tests { t.get_console().borrow_mut().set_size_chars(CharsXY { x: 12345, y: 0 }); t.run("result = SCRCOLS").expect_var("result", 12345i32).check(); - check_expr_compilation_error( - "1:10: In call to SCRCOLS: expected no arguments nor parenthesis", - "SCRCOLS()", - ); - check_expr_compilation_error( - "1:10: In call to SCRCOLS: expected no arguments nor parenthesis", - "SCRCOLS(1)", - ); + check_expr_compilation_error("1:10: SCRCOLS expected no arguments", "SCRCOLS()"); + check_expr_compilation_error("1:10: SCRCOLS expected no arguments", "SCRCOLS(1)"); } #[test] @@ -1073,13 +1032,7 @@ mod tests { t.get_console().borrow_mut().set_size_chars(CharsXY { x: 0, y: 768 }); t.run("result = SCRROWS").expect_var("result", 768i32).check(); - check_expr_compilation_error( - "1:10: In call to SCRROWS: expected no arguments nor parenthesis", - "SCRROWS()", - ); - check_expr_compilation_error( - "1:10: In call to SCRROWS: expected no arguments nor parenthesis", - "SCRROWS(1)", - ); + check_expr_compilation_error("1:10: SCRROWS expected no arguments", "SCRROWS()"); + check_expr_compilation_error("1:10: SCRROWS expected no arguments", "SCRROWS(1)"); } } diff --git a/std/src/data.rs b/std/src/data.rs index d7e25374..bb340114 100644 --- a/std/src/data.rs +++ b/std/src/data.rs @@ -304,15 +304,9 @@ mod tests { #[test] fn test_read_errors() { - check_stmt_compilation_err("1:1: In call to READ: expected vref1[, .., vrefN]", "READ"); - check_stmt_compilation_err( - "1:1: In call to READ: 1:6: Requires a variable reference, not a value", - "READ 3", - ); - check_stmt_compilation_err( - "1:1: In call to READ: expected vref1[, .., vrefN]", - "READ i; j", - ); + check_stmt_compilation_err("1:1: READ expected vref1[, .., vrefN]", "READ"); + check_stmt_compilation_err("1:6: Requires a reference, not a value", "READ 3"); + check_stmt_compilation_err("1:1: READ expected vref1[, .., vrefN]", "READ i; j"); check_stmt_err( "1:11: In call to READ: 1:16: Cannot assign value of type STRING to variable of type INTEGER", @@ -363,6 +357,6 @@ mod tests { #[test] fn test_restore_errors() { - check_stmt_compilation_err("1:1: In call to RESTORE: expected no arguments", "RESTORE 123"); + check_stmt_compilation_err("1:1: RESTORE expected no arguments", "RESTORE 123"); } } diff --git a/std/src/exec.rs b/std/src/exec.rs index e3f82e20..7a347cb2 100644 --- a/std/src/exec.rs +++ b/std/src/exec.rs @@ -206,7 +206,7 @@ mod tests { #[test] fn test_clear_errors() { - check_stmt_compilation_err("1:1: In call to CLEAR: expected no arguments", "CLEAR 123"); + check_stmt_compilation_err("1:1: CLEAR expected no arguments", "CLEAR 123"); } #[test] @@ -224,14 +224,8 @@ mod tests { #[test] fn test_errmsg_errors() { - check_expr_compilation_error( - "1:10: In call to ERRMSG: expected no arguments nor parenthesis", - r#"ERRMSG()"#, - ); - check_expr_compilation_error( - "1:10: In call to ERRMSG: expected no arguments nor parenthesis", - r#"ERRMSG(3)"#, - ); + check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG()"#); + check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG(3)"#); } #[test] @@ -272,13 +266,10 @@ mod tests { #[test] fn test_sleep_errors() { - check_stmt_compilation_err("1:1: In call to SLEEP: expected seconds#", "SLEEP"); - check_stmt_compilation_err("1:1: In call to SLEEP: expected seconds#", "SLEEP 2, 3"); - check_stmt_compilation_err("1:1: In call to SLEEP: expected seconds#", "SLEEP 2; 3"); - check_stmt_compilation_err( - "1:1: In call to SLEEP: 1:7: STRING is not a number", - "SLEEP \"foo\"", - ); + check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP"); + check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3"); + check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3"); + check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\""); check_stmt_err("1:1: In call to SLEEP: 1:7: Sleep time must be positive", "SLEEP -1"); check_stmt_err("1:1: In call to SLEEP: 1:7: Sleep time must be positive", "SLEEP -0.001"); } diff --git a/std/src/gfx.rs b/std/src/gfx.rs index a6766965..2e25c960 100644 --- a/std/src/gfx.rs +++ b/std/src/gfx.rs @@ -660,7 +660,7 @@ mod tests { fn check_errors_two_xy(name: &'static str) { for args in &["1, 2, , 4", "1, 2, 3", "1, 2, 3, 4, 5", "2; 3, 4"] { check_stmt_compilation_err( - format!("1:1: In call to {}: expected x1%, y1%, x2%, y2%", name), + format!("1:1: {} expected x1%, y1%, x2%, y2%", name), &format!("{} {}", name, args), ); } @@ -684,10 +684,7 @@ mod tests { for args in &["\"a\", 1, 1, 1", "1, \"a\", 1, 1", "1, 1, \"a\", 1", "1, 1, 1, \"a\""] { let stmt = &format!("{} {}", name, args); let pos = stmt.find('"').unwrap() + 1; - check_stmt_compilation_err( - format!("1:1: In call to {}: 1:{}: STRING is not a number", name, pos), - stmt, - ); + check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt); } } @@ -695,7 +692,7 @@ mod tests { fn check_errors_xy_radius(name: &'static str) { for args in &["1, , 3", "1, 2", "1, 2, 3, 4", "2; 3, 4"] { check_stmt_compilation_err( - format!("1:1: In call to {}: expected x%, y%, r%", name), + format!("1:1: {} expected x%, y%, r%", name), &format!("{} {}", name, args), ); } @@ -731,10 +728,7 @@ mod tests { for args in &["\"a\", 1, 1", "1, \"a\", 1", "1, 1, \"a\""] { let stmt = &format!("{} {}", name, args); let pos = stmt.find('"').unwrap() + 1; - check_stmt_compilation_err( - format!("1:1: In call to {}: 1:{}: STRING is not a number", name, pos), - stmt, - ); + check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt); } check_stmt_err( @@ -803,14 +797,8 @@ mod tests { "GFX_HEIGHT", ); - check_expr_compilation_error( - "1:10: In call to GFX_HEIGHT: expected no arguments nor parenthesis", - "GFX_HEIGHT()", - ); - check_expr_compilation_error( - "1:10: In call to GFX_HEIGHT: expected no arguments nor parenthesis", - "GFX_HEIGHT(1)", - ); + check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT()"); + check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT(1)"); } #[test] @@ -858,7 +846,7 @@ mod tests { #[test] fn test_gfx_pixel_errors() { for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1", "GFX_PIXEL 1; 2"] { - check_stmt_compilation_err("1:1: In call to GFX_PIXEL: expected x%, y%", cmd); + check_stmt_compilation_err("1:1: GFX_PIXEL expected x%, y%", cmd); } for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] { @@ -873,10 +861,7 @@ mod tests { for cmd in &["GFX_PIXEL \"a\", 1", "GFX_PIXEL 1, \"a\""] { let pos = cmd.find('"').unwrap() + 1; - check_stmt_compilation_err( - format!("1:1: In call to GFX_PIXEL: 1:{}: STRING is not a number", pos), - cmd, - ); + check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), cmd); } } @@ -943,14 +928,8 @@ mod tests { #[test] fn test_gfx_sync_errors() { - check_stmt_compilation_err( - "1:1: In call to GFX_SYNC: expected <> | ", - "GFX_SYNC 2, 3", - ); - check_stmt_compilation_err( - "1:1: In call to GFX_SYNC: 1:10: INTEGER is not a BOOLEAN", - "GFX_SYNC 2", - ); + check_stmt_compilation_err("1:1: GFX_SYNC expected <> | ", "GFX_SYNC 2, 3"); + check_stmt_compilation_err("1:10: expected BOOLEAN but found INTEGER", "GFX_SYNC 2"); } #[test] @@ -964,13 +943,7 @@ mod tests { "GFX_WIDTH", ); - check_expr_compilation_error( - "1:10: In call to GFX_WIDTH: expected no arguments nor parenthesis", - "GFX_WIDTH()", - ); - check_expr_compilation_error( - "1:10: In call to GFX_WIDTH: expected no arguments nor parenthesis", - "GFX_WIDTH(1)", - ); + check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH()"); + check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH(1)"); } } diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index 38c6cf5a..faef31f6 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -403,18 +403,19 @@ mod tests { /// Common checks for pin number validation. /// /// The given input `fmt` string contains the command to test with a placeholder `_PIN` for - /// where the pin number goes. The `prefix` contains a possible prefix for the error messages. - fn check_pin_validation(prefix: &str, fmt: &str) { + /// where the pin number goes. The `short_prefix` and `long_prefix` contain possible prefixes + /// for the error messages. + fn check_pin_validation(short_prefix: &str, long_prefix: &str, fmt: &str) { check_stmt_compilation_err( - format!(r#"{}BOOLEAN is not a number"#, prefix), + format!(r#"{}BOOLEAN is not a number"#, short_prefix), &fmt.replace("_PIN_", "TRUE"), ); check_stmt_err( - format!(r#"{}Pin number 123456789 is too large"#, prefix), + format!(r#"{}Pin number 123456789 is too large"#, long_prefix), &fmt.replace("_PIN_", "123456789"), ); check_stmt_err( - format!(r#"{}Pin number -1 must be positive"#, prefix), + format!(r#"{}Pin number -1 must be positive"#, long_prefix), &fmt.replace("_PIN_", "-1"), ); } @@ -506,24 +507,16 @@ mod tests { #[test] fn test_gpio_setup_errors() { - check_stmt_compilation_err( - "1:1: In call to GPIO_SETUP: expected pin%, mode$", - r#"GPIO_SETUP"#, - ); - check_stmt_compilation_err( - "1:1: In call to GPIO_SETUP: expected pin%, mode$", - r#"GPIO_SETUP 1"#, - ); - check_stmt_compilation_err( - "1:1: In call to GPIO_SETUP: 1:15: INTEGER is not a STRING", - r#"GPIO_SETUP 1; 2"#, + check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP"#); + check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1"#); + check_stmt_compilation_err("1:15: expected STRING but found INTEGER", r#"GPIO_SETUP 1; 2"#); + check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1, 2, 3"#); + + check_pin_validation( + "1:12: ", + "1:1: In call to GPIO_SETUP: 1:12: ", + r#"GPIO_SETUP _PIN_, "IN""#, ); - check_stmt_compilation_err( - "1:1: In call to GPIO_SETUP: expected pin%, mode$", - r#"GPIO_SETUP 1, 2, 3"#, - ); - - check_pin_validation("1:1: In call to GPIO_SETUP: 1:12: ", r#"GPIO_SETUP _PIN_, "IN""#); check_stmt_err( r#"1:1: In call to GPIO_SETUP: 1:15: Unknown pin mode IN-OUT"#, @@ -544,16 +537,10 @@ mod tests { #[test] fn test_gpio_clear_errors() { - check_stmt_compilation_err( - "1:1: In call to GPIO_CLEAR: expected <> | ", - r#"GPIO_CLEAR 1,"#, - ); - check_stmt_compilation_err( - "1:1: In call to GPIO_CLEAR: expected <> | ", - r#"GPIO_CLEAR 1, 2"#, - ); + check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | ", r#"GPIO_CLEAR 1,"#); + check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | ", r#"GPIO_CLEAR 1, 2"#); - check_pin_validation("1:1: In call to GPIO_CLEAR: 1:12: ", r#"GPIO_CLEAR _PIN_"#); + check_pin_validation("1:12: ", "1:1: In call to GPIO_CLEAR: 1:12: ", r#"GPIO_CLEAR _PIN_"#); } #[test] @@ -570,13 +557,14 @@ mod tests { #[test] fn test_gpio_read_errors() { - check_expr_compilation_error("1:10: In call to GPIO_READ: expected pin%", r#"GPIO_READ()"#); - check_expr_compilation_error( - "1:10: In call to GPIO_READ: expected pin%", - r#"GPIO_READ(1, 2)"#, - ); + check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ()"#); + check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ(1, 2)"#); - check_pin_validation("1:5: In call to GPIO_READ: 1:15: ", r#"v = GPIO_READ(_PIN_)"#); + check_pin_validation( + "1:15: ", + "1:5: In call to GPIO_READ: 1:15: ", + r#"v = GPIO_READ(_PIN_)"#, + ); } #[test] @@ -586,27 +574,25 @@ mod tests { #[test] fn test_gpio_write_errors() { + check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE"#); + check_stmt_compilation_err("1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 2,"#); check_stmt_compilation_err( - "1:1: In call to GPIO_WRITE: expected pin%, value?", - r#"GPIO_WRITE"#, - ); - check_stmt_compilation_err( - "1:1: In call to GPIO_WRITE: expected pin%, value?", - r#"GPIO_WRITE 2,"#, - ); - check_stmt_compilation_err( - "1:1: In call to GPIO_WRITE: expected pin%, value?", + "1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 1, TRUE, 2"#, ); check_stmt_compilation_err( - "1:1: In call to GPIO_WRITE: expected pin%, value?", + "1:1: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 1; TRUE"#, ); - check_pin_validation("1:1: In call to GPIO_WRITE: 1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#); + check_pin_validation( + "1:12: ", + "1:1: In call to GPIO_WRITE: 1:12: ", + r#"GPIO_WRITE _PIN_, TRUE"#, + ); check_stmt_compilation_err( - "1:1: In call to GPIO_WRITE: 1:15: INTEGER is not a BOOLEAN", + "1:15: expected BOOLEAN but found INTEGER", r#"GPIO_WRITE 1, 5"#, ); } diff --git a/std/src/help.rs b/std/src/help.rs index 7ed0f524..b38b8447 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -94,7 +94,7 @@ impl Topic for CallableTopic { pager.set_color(Some(TITLE_COLOR), previous.1)?; match self.metadata.return_type() { None => { - if self.metadata.syntax().is_empty() { + if self.metadata.is_argless() { refill_and_page(pager, [self.metadata.name()], " ").await?; } else { refill_and_page( @@ -107,7 +107,6 @@ impl Topic for CallableTopic { } Some(return_type) => { if self.metadata.is_argless() { - debug_assert!(self.metadata.syntax().is_empty()); refill_and_page( pager, [&format!("{}{}", self.metadata.name(), return_type.annotation(),)], @@ -953,16 +952,12 @@ This is the first and only topic with just one line. tester().add_callable(DoNothingCommand::new()).add_callable(EmptyFunction::new()); t.run(r#"HELP foo bar"#).expect_err("1:10: Unexpected value in expression").check(); - t.run(r#"HELP foo"#) - .expect_compilation_err("1:1: In call to HELP: 1:6: Undefined variable foo") - .check(); + t.run(r#"HELP foo"#).expect_compilation_err("1:6: Undefined symbol FOO").check(); t.run(r#"HELP "foo", 3"#) - .expect_compilation_err("1:1: In call to HELP: expected <> | ") - .check(); - t.run(r#"HELP 3"#) - .expect_compilation_err("1:1: In call to HELP: 1:6: INTEGER is not a STRING") + .expect_compilation_err("1:1: HELP expected <> | ") .check(); + t.run(r#"HELP 3"#).expect_compilation_err("1:6: expected STRING but found INTEGER").check(); t.run(r#"HELP "lang%""#) .expect_err("1:1: In call to HELP: 1:6: Unknown help topic lang%") diff --git a/std/src/numerics.rs b/std/src/numerics.rs index e218035f..269dfe22 100644 --- a/std/src/numerics.rs +++ b/std/src/numerics.rs @@ -774,12 +774,9 @@ mod tests { check_expr_ok_with_vars(123f64.atan(), "ATN(a)", [("a", 123i32.into())]); - check_expr_compilation_error("1:10: In call to ATN: expected n#", "ATN()"); - check_expr_compilation_error( - "1:10: In call to ATN: 1:14: BOOLEAN is not a number", - "ATN(FALSE)", - ); - check_expr_compilation_error("1:10: In call to ATN: expected n#", "ATN(3, 4)"); + check_expr_compilation_error("1:10: ATN expected n#", "ATN()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "ATN(FALSE)"); + check_expr_compilation_error("1:10: ATN expected n#", "ATN(3, 4)"); } #[test] @@ -791,12 +788,9 @@ mod tests { check_expr_ok_with_vars(1, "CINT(d)", [("d", 0.9f64.into())]); - check_expr_compilation_error("1:10: In call to CINT: expected expr#", "CINT()"); - check_expr_compilation_error( - "1:10: In call to CINT: 1:15: BOOLEAN is not a number", - "CINT(FALSE)", - ); - check_expr_compilation_error("1:10: In call to CINT: expected expr#", "CINT(3.0, 4)"); + check_expr_compilation_error("1:10: CINT expected expr#", "CINT()"); + check_expr_compilation_error("1:15: BOOLEAN is not a number", "CINT(FALSE)"); + check_expr_compilation_error("1:10: CINT expected expr#", "CINT(3.0, 4)"); check_expr_error( "1:10: In call to CINT: 1:15: Cannot cast -1234567890123456 to integer due to overflow", @@ -811,12 +805,9 @@ mod tests { check_expr_ok_with_vars(123f64.cos(), "COS(i)", [("i", 123i32.into())]); - check_expr_compilation_error("1:10: In call to COS: expected angle#", "COS()"); - check_expr_compilation_error( - "1:10: In call to COS: 1:14: BOOLEAN is not a number", - "COS(FALSE)", - ); - check_expr_compilation_error("1:10: In call to COS: expected angle#", "COS(3, 4)"); + check_expr_compilation_error("1:10: COS expected angle#", "COS()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "COS(FALSE)"); + check_expr_compilation_error("1:10: COS expected angle#", "COS(3, 4)"); } #[test] @@ -837,8 +828,8 @@ mod tests { #[test] fn test_deg_rad_errors() { - check_stmt_compilation_err("1:1: In call to DEG: expected no arguments", "DEG 1"); - check_stmt_compilation_err("1:1: In call to RAD: expected no arguments", "RAD 1"); + check_stmt_compilation_err("1:1: DEG expected no arguments", "DEG 1"); + check_stmt_compilation_err("1:1: RAD expected no arguments", "RAD 1"); } #[test] @@ -850,12 +841,9 @@ mod tests { check_expr_ok_with_vars(0, "INT(d)", [("d", 0.9f64.into())]); - check_expr_compilation_error("1:10: In call to INT: expected expr#", "INT()"); - check_expr_compilation_error( - "1:10: In call to INT: 1:14: BOOLEAN is not a number", - "INT(FALSE)", - ); - check_expr_compilation_error("1:10: In call to INT: expected expr#", "INT(3.0, 4)"); + check_expr_compilation_error("1:10: INT expected expr#", "INT()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "INT(FALSE)"); + check_expr_compilation_error("1:10: INT expected expr#", "INT(3.0, 4)"); check_expr_error( "1:10: In call to INT: 1:14: Cannot cast -1234567890123456 to integer due to overflow", @@ -887,14 +875,8 @@ mod tests { [("i", 5i32.into()), ("j", 3i32.into()), ("k", 4i32.into())], ); - check_expr_compilation_error( - "1:10: In call to MAX: expected expr1#[, .., exprN#]", - "MAX()", - ); - check_expr_compilation_error( - "1:10: In call to MAX: 1:14: BOOLEAN is not a number", - "MAX(FALSE)", - ); + check_expr_compilation_error("1:10: MAX expected expr1#[, .., exprN#]", "MAX()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "MAX(FALSE)"); } #[test] @@ -921,28 +903,16 @@ mod tests { [("i", 5i32.into()), ("j", 3i32.into()), ("k", 4i32.into())], ); - check_expr_compilation_error( - "1:10: In call to MIN: expected expr1#[, .., exprN#]", - "MIN()", - ); - check_expr_compilation_error( - "1:10: In call to MIN: 1:14: BOOLEAN is not a number", - "MIN(FALSE)", - ); + check_expr_compilation_error("1:10: MIN expected expr1#[, .., exprN#]", "MIN()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "MIN(FALSE)"); } #[test] fn test_pi() { check_expr_ok(std::f64::consts::PI, "PI"); - check_expr_compilation_error( - "1:10: In call to PI: expected no arguments nor parenthesis", - "PI()", - ); - check_expr_compilation_error( - "1:10: In call to PI: expected no arguments nor parenthesis", - "PI(3)", - ); + check_expr_compilation_error("1:10: PI expected no arguments", "PI()"); + check_expr_compilation_error("1:10: PI expected no arguments", "PI(3)"); } #[test] @@ -966,21 +936,12 @@ mod tests { t.run("result = RND(1)").expect_var("result", 0.7097578208683426).check(); - check_expr_compilation_error("1:10: In call to RND: expected <> | ", "RND(1, 7)"); - check_expr_compilation_error( - "1:10: In call to RND: 1:14: BOOLEAN is not a number", - "RND(FALSE)", - ); + check_expr_compilation_error("1:10: RND expected <> | ", "RND(1, 7)"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "RND(FALSE)"); check_expr_error("1:10: In call to RND: 1:14: n% cannot be negative", "RND(-1)"); - check_stmt_compilation_err( - "1:1: In call to RANDOMIZE: expected <> | ", - "RANDOMIZE ,", - ); - check_stmt_compilation_err( - "1:1: In call to RANDOMIZE: 1:11: BOOLEAN is not a number", - "RANDOMIZE TRUE", - ); + check_stmt_compilation_err("1:1: RANDOMIZE expected <> | ", "RANDOMIZE ,"); + check_stmt_compilation_err("1:11: BOOLEAN is not a number", "RANDOMIZE TRUE"); } #[test] @@ -990,12 +951,9 @@ mod tests { check_expr_ok_with_vars(123f64.sin(), "SIN(i)", [("i", 123i32.into())]); - check_expr_compilation_error("1:10: In call to SIN: expected angle#", "SIN()"); - check_expr_compilation_error( - "1:10: In call to SIN: 1:14: BOOLEAN is not a number", - "SIN(FALSE)", - ); - check_expr_compilation_error("1:10: In call to SIN: expected angle#", "SIN(3, 4)"); + check_expr_compilation_error("1:10: SIN expected angle#", "SIN()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "SIN(FALSE)"); + check_expr_compilation_error("1:10: SIN expected angle#", "SIN(3, 4)"); } #[test] @@ -1007,12 +965,9 @@ mod tests { check_expr_ok_with_vars(9f64.sqrt(), "SQR(i)", [("i", 9i32.into())]); - check_expr_compilation_error("1:10: In call to SQR: expected num#", "SQR()"); - check_expr_compilation_error( - "1:10: In call to SQR: 1:14: BOOLEAN is not a number", - "SQR(FALSE)", - ); - check_expr_compilation_error("1:10: In call to SQR: expected num#", "SQR(3, 4)"); + check_expr_compilation_error("1:10: SQR expected num#", "SQR()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "SQR(FALSE)"); + check_expr_compilation_error("1:10: SQR expected num#", "SQR(3, 4)"); check_expr_error( "1:10: In call to SQR: 1:14: Cannot take square root of a negative number", "SQR(-3)", @@ -1030,11 +985,8 @@ mod tests { check_expr_ok_with_vars(123f64.tan(), "TAN(i)", [("i", 123i32.into())]); - check_expr_compilation_error("1:10: In call to TAN: expected angle#", "TAN()"); - check_expr_compilation_error( - "1:10: In call to TAN: 1:14: BOOLEAN is not a number", - "TAN(FALSE)", - ); - check_expr_compilation_error("1:10: In call to TAN: expected angle#", "TAN(3, 4)"); + check_expr_compilation_error("1:10: TAN expected angle#", "TAN()"); + check_expr_compilation_error("1:14: BOOLEAN is not a number", "TAN(FALSE)"); + check_expr_compilation_error("1:10: TAN expected angle#", "TAN(3, 4)"); } } diff --git a/std/src/program.rs b/std/src/program.rs index 38454e05..d1e22e35 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -664,7 +664,7 @@ mod tests { Tester::default() .run("KILL") - .expect_compilation_err("1:1: In call to KILL: expected filename$") + .expect_compilation_err("1:1: KILL expected filename$") .check(); check_stmt_err("1:1: In call to KILL: Entry not found", r#"KILL "missing-file""#); @@ -730,7 +730,7 @@ mod tests { #[test] fn test_disasm_errors() { - check_stmt_compilation_err("1:1: In call to DISASM: expected no arguments", "DISASM 2"); + check_stmt_compilation_err("1:1: DISASM expected no arguments", "DISASM 2"); } #[test] @@ -745,7 +745,7 @@ mod tests { #[test] fn test_edit_errors() { - check_stmt_compilation_err("1:1: In call to EDIT: expected no arguments", "EDIT 1"); + check_stmt_compilation_err("1:1: EDIT expected no arguments", "EDIT 1"); } #[test] @@ -775,7 +775,7 @@ mod tests { #[test] fn test_list_errors() { - check_stmt_compilation_err("1:1: In call to LIST: expected no arguments", "LIST 2"); + check_stmt_compilation_err("1:1: LIST expected no arguments", "LIST 2"); } #[test] @@ -882,8 +882,7 @@ mod tests { Tester::default() .run(format!("{} 3", cmd)) .expect_compilation_err(format!( - "1:1: In call to {}: 1:{}: INTEGER is not a STRING", - cmd, + "1:{}: expected STRING but found INTEGER", cmd.len() + 2, )) .check(); @@ -907,7 +906,7 @@ mod tests { Tester::default() .run("LOAD") - .expect_compilation_err("1:1: In call to LOAD: expected filename$") + .expect_compilation_err("1:1: LOAD expected filename$") .check(); check_stmt_err("1:1: In call to LOAD: Entry not found", r#"LOAD "missing-file""#); @@ -999,7 +998,7 @@ mod tests { #[test] fn test_new_errors() { - check_stmt_compilation_err("1:1: In call to NEW: expected no arguments", "NEW 10"); + check_stmt_compilation_err("1:1: NEW expected no arguments", "NEW 10"); } #[test] @@ -1038,7 +1037,7 @@ mod tests { #[test] fn test_run_errors() { - check_stmt_compilation_err("1:1: In call to RUN: expected no arguments", "RUN 10"); + check_stmt_compilation_err("1:1: RUN expected no arguments", "RUN 10"); } #[test] @@ -1088,7 +1087,7 @@ mod tests { Tester::default() .run("SAVE 2, 3") - .expect_compilation_err("1:1: In call to SAVE: expected <> | ") + .expect_compilation_err("1:1: SAVE expected <> | ") .check(); } } diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index d2813d64..079ca1e8 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -423,9 +423,9 @@ mod tests { #[test] fn test_cd_errors() { check_stmt_err("1:1: In call to CD: Drive 'A' is not mounted", "CD \"A:\""); - check_stmt_compilation_err("1:1: In call to CD: expected path$", "CD"); - check_stmt_compilation_err("1:1: In call to CD: expected path$", "CD 2, 3"); - check_stmt_compilation_err("1:1: In call to CD: 1:4: INTEGER is not a STRING", "CD 2"); + check_stmt_compilation_err("1:1: CD expected path$", "CD"); + check_stmt_compilation_err("1:1: CD expected path$", "CD 2, 3"); + check_stmt_compilation_err("1:4: expected STRING but found INTEGER", "CD 2"); } #[test] @@ -637,8 +637,8 @@ mod tests { #[test] fn test_dir_errors() { - check_stmt_compilation_err("1:1: In call to DIR: expected <> | ", "DIR 2, 3"); - check_stmt_compilation_err("1:1: In call to DIR: 1:5: INTEGER is not a STRING", "DIR 2"); + check_stmt_compilation_err("1:1: DIR expected <> | ", "DIR 2, 3"); + check_stmt_compilation_err("1:5: expected STRING but found INTEGER", "DIR 2"); } #[test] @@ -684,23 +684,14 @@ mod tests { #[test] fn test_mount_errors() { + check_stmt_compilation_err("1:1: MOUNT expected <> | ", "MOUNT 1"); check_stmt_compilation_err( - "1:1: In call to MOUNT: expected <> | ", - "MOUNT 1", - ); - check_stmt_compilation_err( - "1:1: In call to MOUNT: expected <> | ", + "1:1: MOUNT expected <> | ", "MOUNT 1, 2, 3", ); - check_stmt_compilation_err( - "1:1: In call to MOUNT: 1:14: INTEGER is not a STRING", - r#"MOUNT "a" AS 1"#, - ); - check_stmt_compilation_err( - "1:1: In call to MOUNT: 1:7: INTEGER is not a STRING", - r#"MOUNT 1 AS "a""#, - ); + check_stmt_compilation_err("1:14: expected STRING but found INTEGER", r#"MOUNT "a" AS 1"#); + check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"MOUNT 1 AS "a""#); check_stmt_err( "1:1: In call to MOUNT: Invalid drive name 'a:'", @@ -768,13 +759,10 @@ mod tests { #[test] fn test_unmount_errors() { - check_stmt_compilation_err("1:1: In call to UNMOUNT: expected drive_name$", "UNMOUNT"); - check_stmt_compilation_err("1:1: In call to UNMOUNT: expected drive_name$", "UNMOUNT 2, 3"); + check_stmt_compilation_err("1:1: UNMOUNT expected drive_name$", "UNMOUNT"); + check_stmt_compilation_err("1:1: UNMOUNT expected drive_name$", "UNMOUNT 2, 3"); - check_stmt_compilation_err( - "1:1: In call to UNMOUNT: 1:9: INTEGER is not a STRING", - "UNMOUNT 1", - ); + check_stmt_compilation_err("1:9: expected STRING but found INTEGER", "UNMOUNT 1"); check_stmt_err("1:1: In call to UNMOUNT: Invalid drive name 'a:'", "UNMOUNT \"a:\""); check_stmt_err("1:1: In call to UNMOUNT: Drive 'a' is not mounted", "UNMOUNT \"a\""); diff --git a/std/src/strings.rs b/std/src/strings.rs index f094d0b0..bf6e9d23 100644 --- a/std/src/strings.rs +++ b/std/src/strings.rs @@ -710,12 +710,9 @@ mod tests { check_expr_ok_with_vars('a' as i32, r#"ASC(s)"#, [("s", "a".into())]); - check_expr_compilation_error("1:10: In call to ASC: expected char$", r#"ASC()"#); - check_expr_compilation_error( - "1:10: In call to ASC: 1:14: INTEGER is not a STRING", - r#"ASC(3)"#, - ); - check_expr_compilation_error("1:10: In call to ASC: expected char$", r#"ASC("a", 1)"#); + check_expr_compilation_error("1:10: ASC expected char$", r#"ASC()"#); + check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"ASC(3)"#); + check_expr_compilation_error("1:10: ASC expected char$", r#"ASC("a", 1)"#); check_expr_error( "1:10: In call to ASC: 1:14: Input string \"\" must be 1-character long", r#"ASC("")"#, @@ -735,12 +732,9 @@ mod tests { check_expr_ok_with_vars(" ", r#"CHR(i)"#, [("i", 32.into())]); - check_expr_compilation_error("1:10: In call to CHR: expected code%", r#"CHR()"#); - check_expr_compilation_error( - "1:10: In call to CHR: 1:14: BOOLEAN is not a number", - r#"CHR(FALSE)"#, - ); - check_expr_compilation_error("1:10: In call to CHR: expected code%", r#"CHR("a", 1)"#); + check_expr_compilation_error("1:10: CHR expected code%", r#"CHR()"#); + check_expr_compilation_error("1:14: BOOLEAN is not a number", r#"CHR(FALSE)"#); + check_expr_compilation_error("1:10: CHR expected code%", r#"CHR("a", 1)"#); check_expr_error( "1:10: In call to CHR: 1:14: Character code -1 must be positive", r#"CHR(-1)"#, @@ -767,19 +761,10 @@ mod tests { check_expr_ok_with_vars("abc", r#"LEFT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]); - check_expr_compilation_error("1:10: In call to LEFT: expected expr$, n%", r#"LEFT()"#); - check_expr_compilation_error( - "1:10: In call to LEFT: expected expr$, n%", - r#"LEFT("", 1, 2)"#, - ); - check_expr_compilation_error( - "1:10: In call to LEFT: 1:15: INTEGER is not a STRING", - r#"LEFT(1, 2)"#, - ); - check_expr_compilation_error( - "1:10: In call to LEFT: 1:19: STRING is not a number", - r#"LEFT("", "")"#, - ); + check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT()"#); + check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT("", 1, 2)"#); + check_expr_compilation_error("1:15: expected STRING but found INTEGER", r#"LEFT(1, 2)"#); + check_expr_compilation_error("1:19: STRING is not a number", r#"LEFT("", "")"#); check_expr_error( "1:10: In call to LEFT: 1:25: n% cannot be negative", r#"LEFT("abcdef", -5)"#, @@ -794,12 +779,9 @@ mod tests { check_expr_ok_with_vars(4, r#"LEN(s)"#, [("s", "1234".into())]); - check_expr_compilation_error("1:10: In call to LEN: expected expr$", r#"LEN()"#); - check_expr_compilation_error( - "1:10: In call to LEN: 1:14: INTEGER is not a STRING", - r#"LEN(3)"#, - ); - check_expr_compilation_error("1:10: In call to LEN: expected expr$", r#"LEN(" ", 1)"#); + check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN()"#); + check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"LEN(3)"#); + check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN(" ", 1)"#); } #[test] @@ -811,12 +793,9 @@ mod tests { check_expr_ok_with_vars("foo ", r#"LTRIM(s)"#, [("s", " foo ".into())]); - check_expr_compilation_error("1:10: In call to LTRIM: expected expr$", r#"LTRIM()"#); - check_expr_compilation_error( - "1:10: In call to LTRIM: 1:16: INTEGER is not a STRING", - r#"LTRIM(3)"#, - ); - check_expr_compilation_error("1:10: In call to LTRIM: expected expr$", r#"LTRIM(" ", 1)"#); + check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM()"#); + check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"LTRIM(3)"#); + check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM(" ", 1)"#); } #[test] @@ -839,25 +818,19 @@ mod tests { ); check_expr_compilation_error( - "1:10: In call to MID: expected | ", + "1:10: MID expected | ", r#"MID()"#, ); check_expr_compilation_error( - "1:10: In call to MID: expected | ", + "1:10: MID expected | ", r#"MID(3)"#, ); check_expr_compilation_error( - "1:10: In call to MID: expected | ", + "1:10: MID expected | ", r#"MID(" ", 1, 1, 10)"#, ); - check_expr_compilation_error( - "1:10: In call to MID: 1:19: STRING is not a number", - r#"MID(" ", "1", 2)"#, - ); - check_expr_compilation_error( - "1:10: In call to MID: 1:22: STRING is not a number", - r#"MID(" ", 1, "2")"#, - ); + check_expr_compilation_error("1:19: STRING is not a number", r#"MID(" ", "1", 2)"#); + check_expr_compilation_error("1:22: STRING is not a number", r#"MID(" ", 1, "2")"#); check_expr_error( "1:10: In call to MID: 1:24: start% cannot be negative", r#"MID("abcdef", -5, 10)"#, @@ -878,19 +851,10 @@ mod tests { check_expr_ok_with_vars("def", r#"RIGHT(s, i)"#, [("s", "abcdef".into()), ("i", 3.into())]); - check_expr_compilation_error("1:10: In call to RIGHT: expected expr$, n%", r#"RIGHT()"#); - check_expr_compilation_error( - "1:10: In call to RIGHT: expected expr$, n%", - r#"RIGHT("", 1, 2)"#, - ); - check_expr_compilation_error( - "1:10: In call to RIGHT: 1:16: INTEGER is not a STRING", - r#"RIGHT(1, 2)"#, - ); - check_expr_compilation_error( - "1:10: In call to RIGHT: 1:20: STRING is not a number", - r#"RIGHT("", "")"#, - ); + check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT()"#); + check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT("", 1, 2)"#); + check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RIGHT(1, 2)"#); + check_expr_compilation_error("1:20: STRING is not a number", r#"RIGHT("", "")"#); check_expr_error( "1:10: In call to RIGHT: 1:26: n% cannot be negative", r#"RIGHT("abcdef", -5)"#, @@ -906,12 +870,9 @@ mod tests { check_expr_ok_with_vars(" foo", r#"RTRIM(s)"#, [("s", " foo ".into())]); - check_expr_compilation_error("1:10: In call to RTRIM: expected expr$", r#"RTRIM()"#); - check_expr_compilation_error( - "1:10: In call to RTRIM: 1:16: INTEGER is not a STRING", - r#"RTRIM(3)"#, - ); - check_expr_compilation_error("1:10: In call to RTRIM: expected expr$", r#"RTRIM(" ", 1)"#); + check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM()"#); + check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RTRIM(3)"#); + check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM(" ", 1)"#); } #[test] @@ -933,8 +894,8 @@ mod tests { check_expr_ok_with_vars(" 1", r#"STR(i)"#, [("i", 1.into())]); - check_expr_compilation_error("1:10: In call to STR: expected expr", r#"STR()"#); - check_expr_compilation_error("1:10: In call to STR: expected expr", r#"STR(" ", 1)"#); + check_expr_compilation_error("1:10: STR expected expr", r#"STR()"#); + check_expr_compilation_error("1:10: STR expected expr", r#"STR(" ", 1)"#); } #[test] diff --git a/std/tests/script-errors.err b/std/tests/script-errors.err index 285c870a..6fe0307c 100644 --- a/std/tests/script-errors.err +++ b/std/tests/script-errors.err @@ -1 +1 @@ -ERROR: 21:1: Unknown builtin HELP +ERROR: 21:1: Undefined symbol HELP From 58785cca2c8ab4495375ce55acd571feb81b6500 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 9 Nov 2024 09:00:55 -0800 Subject: [PATCH 008/110] Merge CallError::{Argument,Syntax}Error This is the first step in eliminating differences between CallError and exec::Error with the goal of unifying them. --- client/src/cmds.rs | 2 +- core/examples/dsl.rs | 4 ++-- core/src/exec.rs | 47 +++++++++++++++-------------------------- core/src/syms.rs | 8 ++----- core/src/testutils.rs | 6 ++---- std/src/arrays.rs | 8 +++---- std/src/console/cmds.rs | 8 +++---- std/src/data.rs | 2 +- std/src/exec.rs | 2 +- std/src/gfx.rs | 6 +++--- std/src/gpio/mod.rs | 9 +++----- std/src/help.rs | 4 ++-- std/src/numerics.rs | 9 ++++---- std/src/strings.rs | 16 +++++++------- 14 files changed, 54 insertions(+), 77 deletions(-) diff --git a/client/src/cmds.rs b/client/src/cmds.rs index 247360a8..6720b027 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -320,7 +320,7 @@ impl ShareCommand { (username, "-r") if !username.is_empty() => remove.add_reader(username), (username, "-R") if !username.is_empty() => remove.add_reader(username), (username, change) => { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( acl_pos, format!( "Invalid ACL '{}{}': must be of the form \"username+r\" or \"username-r\"", diff --git a/core/examples/dsl.rs b/core/examples/dsl.rs index 5bc817f5..1558425d 100644 --- a/core/examples/dsl.rs +++ b/core/examples/dsl.rs @@ -123,14 +123,14 @@ impl Callable for SwitchLightCommand { let lights = &mut *self.lights.borrow_mut(); if i < 1 { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( ipos, "Light id cannot be zero or negative".to_owned(), )); } let i = i as usize; if i > lights.len() { - return Err(CallError::ArgumentError(ipos, "Light id out of range".to_owned())); + return Err(CallError::SyntaxError(ipos, "Light id out of range".to_owned())); } if lights[i - 1] { println!("Turning light {} off", i); diff --git a/core/src/exec.rs b/core/src/exec.rs index 8e039037..99f94d3a 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -62,9 +62,6 @@ impl Error { // somehow unified with the equivalent function in eval::Error. fn from_call_error(md: &CallableMetadata, e: CallError, pos: LineCol) -> Self { match e { - CallError::ArgumentError(pos2, e) => { - Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } CallError::EvalError(pos2, e) => { if !md.is_function() { Self::EvalError(pos2, e) @@ -80,13 +77,9 @@ impl Error { io::Error::new(e.kind(), format!("In call to {}: {}", md.name(), e)), ), CallError::NestedError(e) => e, - CallError::SyntaxError if md.syntax().is_empty() => { - Self::SyntaxError(pos, format!("In call to {}: expected no arguments", md.name())) + CallError::SyntaxError(pos2, e) => { + Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) } - CallError::SyntaxError => Self::SyntaxError( - pos, - format!("In call to {}: expected {}", md.name(), md.syntax()), - ), } } @@ -2784,12 +2777,12 @@ mod tests { r#" ON ERROR GOTO 100 OUT 1 - OUT RAISEF("syntax") + OUT RAISEF("internal") OUT 2 100 OUT LAST_ERROR "#, &[], - &["1", "4:17: In call to RAISEF: expected arg$"], + &["1", "4:17: In call to RAISEF: 4:24: Some internal error"], ); } @@ -2799,13 +2792,13 @@ mod tests { r#" ON ERROR GOTO @foo OUT 1 - OUT RAISEF("syntax") + OUT RAISEF("internal") OUT 2 @foo OUT LAST_ERROR "#, &[], - &["1", "4:17: In call to RAISEF: expected arg$"], + &["1", "4:17: In call to RAISEF: 4:24: Some internal error"], ); } @@ -2815,15 +2808,15 @@ mod tests { r#" ON ERROR GOTO @foo OUT 1 - OUT RAISEF("syntax") + OUT RAISEF("internal") @foo ON ERROR GOTO 0 OUT 2 - OUT RAISEF("syntax") + OUT RAISEF("internal") "#, &[], &["1", "2"], - "8:17: In call to RAISEF: expected arg$", + "8:17: In call to RAISEF: 8:24: Some internal error", ); } @@ -2833,11 +2826,11 @@ mod tests { r#" ON ERROR RESUME NEXT OUT 1 - OUT RAISEF("syntax") + OUT RAISEF("internal") OUT LAST_ERROR "#, &[], - &["1", "4:17: In call to RAISEF: expected arg$"], + &["1", "4:17: In call to RAISEF: 4:24: Some internal error"], ); } @@ -2847,11 +2840,11 @@ mod tests { r#" ON ERROR RESUME NEXT OUT 1 - RAISE "syntax" + RAISE "internal" OUT LAST_ERROR "#, &[], - &["1", "4:13: In call to RAISE: expected arg$"], + &["1", "4:13: In call to RAISE: 4:19: Some internal error"], ); } @@ -2860,10 +2853,10 @@ mod tests { do_ok_test( r#" ON ERROR RESUME NEXT - OUT 1: OUT RAISEF("syntax"): OUT LAST_ERROR + OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR "#, &[], - &["1", "3:24: In call to RAISEF: expected arg$"], + &["1", "3:24: In call to RAISEF: 3:31: Some internal error"], ); } @@ -2872,10 +2865,10 @@ mod tests { do_ok_test( r#" ON ERROR RESUME NEXT - OUT 1: RAISE "syntax": OUT LAST_ERROR + OUT 1: RAISE "internal": OUT LAST_ERROR "#, &[], - &["1", "3:20: In call to RAISE: expected arg$"], + &["1", "3:20: In call to RAISE: 3:26: Some internal error"], ); } @@ -2904,12 +2897,6 @@ mod tests { &[], &["1:27: In call to RAISEF: Some I/O error"], ); - - do_ok_test( - r#"ON ERROR RESUME NEXT: OUT RAISEF("syntax"): OUT LAST_ERROR"#, - &[], - &["1:27: In call to RAISEF: expected arg$"], - ); } #[test] diff --git a/core/src/syms.rs b/core/src/syms.rs index 2485efc4..67be8b72 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -36,9 +36,6 @@ use std::str::Lines; /// within the evaluation logic. #[derive(Debug)] pub enum CallError { - /// A specific parameter had an invalid value. - ArgumentError(LineCol, String), - /// Error while evaluating input arguments. EvalError(LineCol, String), @@ -52,9 +49,8 @@ pub enum CallError { // TODO(jmmv): Consider unifying `CallError` with `exec::Error`. NestedError(exec::Error), - /// General mismatch of parameters given to the function with expectations (different numbers, - /// invalid types). - SyntaxError, + /// A specific parameter had an invalid value. + SyntaxError(LineCol, String), } impl From for CallError { diff --git a/core/src/testutils.rs b/core/src/testutils.rs index 0bc5ebe5..6bb9ea5c 100644 --- a/core/src/testutils.rs +++ b/core/src/testutils.rs @@ -156,11 +156,10 @@ impl Callable for RaisefFunction { assert_eq!(1, scope.nargs()); let (arg, pos) = scope.pop_string_with_pos(); match arg.as_str() { - "argument" => Err(CallError::ArgumentError(pos, "Bad argument".to_owned())), + "argument" => Err(CallError::SyntaxError(pos, "Bad argument".to_owned())), "eval" => Err(CallError::EvalError(pos, "Some eval error".to_owned())), "internal" => Err(CallError::InternalError(pos, "Some internal error".to_owned())), "io" => Err(io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()).into()), - "syntax" => Err(CallError::SyntaxError), _ => panic!("Invalid arguments"), } } @@ -197,11 +196,10 @@ impl Callable for RaiseCommand { assert_eq!(1, scope.nargs()); let (arg, pos) = scope.pop_string_with_pos(); match arg.as_str() { - "argument" => Err(CallError::ArgumentError(pos, "Bad argument".to_owned())), + "argument" => Err(CallError::SyntaxError(pos, "Bad argument".to_owned())), "eval" => Err(CallError::EvalError(pos, "Some eval error".to_owned())), "internal" => Err(CallError::InternalError(pos, "Some internal error".to_owned())), "io" => Err(io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()).into()), - "syntax" => Err(CallError::SyntaxError), _ => panic!("Invalid arguments"), } } diff --git a/std/src/arrays.rs b/std/src/arrays.rs index 0ee1c8fc..29df4685 100644 --- a/std/src/arrays.rs +++ b/std/src/arrays.rs @@ -43,7 +43,7 @@ fn parse_bound_args<'a>( let arrayref = VarRef::new(arrayname.to_string(), Some(arraytype)); let array = match symbols .get(&arrayref) - .map_err(|e| CallError::ArgumentError(arraypos, format!("{}", e)))? + .map_err(|e| CallError::SyntaxError(arraypos, format!("{}", e)))? { Some(Symbol::Array(array)) => array, _ => unreachable!(), @@ -53,12 +53,12 @@ fn parse_bound_args<'a>( let (i, pos) = scope.pop_integer_with_pos(); if i < 0 { - return Err(CallError::ArgumentError(pos, format!("Dimension {} must be positive", i))); + return Err(CallError::SyntaxError(pos, format!("Dimension {} must be positive", i))); } let i = i as usize; if i > array.dimensions().len() { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( pos, format!( "Array {} has only {} dimensions but asked for {}", @@ -73,7 +73,7 @@ fn parse_bound_args<'a>( debug_assert_eq!(0, scope.nargs()); if array.dimensions().len() > 1 { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( arraypos, "Requires a dimension for multidimensional arrays".to_owned(), )); diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index b951f82d..a08c5ece 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -162,7 +162,7 @@ impl Callable for ColorCommand { if i >= 0 && i <= u8::MAX as i32 { Ok(Some(i as u8)) } else { - Err(CallError::ArgumentError(pos, "Color out of range".to_owned())) + Err(CallError::SyntaxError(pos, "Color out of range".to_owned())) } } @@ -459,7 +459,7 @@ impl Callable for LocateCommand { fn get_coord((i, pos): (i32, LineCol), name: &str) -> Result<(u16, LineCol), CallError> { match u16::try_from(i) { Ok(v) => Ok((v, pos)), - Err(_) => Err(CallError::ArgumentError(pos, format!("{} out of range", name))), + Err(_) => Err(CallError::SyntaxError(pos, format!("{} out of range", name))), } } @@ -471,13 +471,13 @@ impl Callable for LocateCommand { let size = console.size_chars()?; if column >= size.x { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( column_pos, format!("Column {} exceeds visible range of {}", column, size.x - 1), )); } if row >= size.y { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( row_pos, format!("Row {} exceeds visible range of {}", row, size.y - 1), )); diff --git a/std/src/data.rs b/std/src/data.rs index bb340114..9f51cb12 100644 --- a/std/src/data.rs +++ b/std/src/data.rs @@ -119,7 +119,7 @@ impl Callable for ReadCommand { machine .get_mut_symbols() .set_var(&vref, datum) - .map_err(|e| CallError::ArgumentError(pos, format!("{}", e)))?; + .map_err(|e| CallError::SyntaxError(pos, format!("{}", e)))?; } Ok(()) diff --git a/std/src/exec.rs b/std/src/exec.rs index 7a347cb2..94304302 100644 --- a/std/src/exec.rs +++ b/std/src/exec.rs @@ -167,7 +167,7 @@ impl Callable for SleepCommand { let (n, pos) = scope.pop_double_with_pos(); if n < 0.0 { - return Err(CallError::ArgumentError(pos, "Sleep time must be positive".to_owned())); + return Err(CallError::SyntaxError(pos, "Sleep time must be positive".to_owned())); } (self.sleep_fn)(Duration::from_secs_f64(n), pos).await diff --git a/std/src/gfx.rs b/std/src/gfx.rs index 2e25c960..3257a03a 100644 --- a/std/src/gfx.rs +++ b/std/src/gfx.rs @@ -40,7 +40,7 @@ described in this section."; fn parse_coordinate(i: i32, pos: LineCol) -> Result { match i16::try_from(i) { Ok(i) => Ok(i), - Err(_) => Err(CallError::ArgumentError(pos, format!("Coordinate {} out of range", i))), + Err(_) => Err(CallError::SyntaxError(pos, format!("Coordinate {} out of range", i))), } } @@ -59,9 +59,9 @@ fn parse_radius(i: i32, pos: LineCol) -> Result { match u16::try_from(i) { Ok(i) => Ok(i), Err(_) if i < 0 => { - Err(CallError::ArgumentError(pos, format!("Radius {} must be positive", i))) + Err(CallError::SyntaxError(pos, format!("Radius {} must be positive", i))) } - Err(_) => Err(CallError::ArgumentError(pos, format!("Radius {} out of range", i))), + Err(_) => Err(CallError::SyntaxError(pos, format!("Radius {} out of range", i))), } } diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index faef31f6..fe7b279e 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -46,13 +46,10 @@ impl Pin { /// Creates a new pin number from an EndBASIC integer value. fn from_i32(i: i32, pos: LineCol) -> Result { if i < 0 { - return Err(CallError::ArgumentError( - pos, - format!("Pin number {} must be positive", i), - )); + return Err(CallError::SyntaxError(pos, format!("Pin number {} must be positive", i))); } if i > u8::MAX as i32 { - return Err(CallError::ArgumentError(pos, format!("Pin number {} is too large", i))); + return Err(CallError::SyntaxError(pos, format!("Pin number {} is too large", i))); } Ok(Self(i as u8)) } @@ -82,7 +79,7 @@ impl PinMode { "IN-PULL-UP" => Ok(PinMode::InPullUp), "IN-PULL-DOWN" => Ok(PinMode::InPullDown), "OUT" => Ok(PinMode::Out), - s => Err(CallError::ArgumentError(pos, format!("Unknown pin mode {}", s))), + s => Err(CallError::SyntaxError(pos, format!("Unknown pin mode {}", s))), } } } diff --git a/std/src/help.rs b/std/src/help.rs index b38b8447..f0864a66 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -381,7 +381,7 @@ impl Topics { _ => { let completions: Vec = children.iter().map(|(name, _topic)| (*name).to_owned()).collect(); - Err(CallError::ArgumentError( + Err(CallError::SyntaxError( pos, format!( "Ambiguous help topic {}; candidates are: {}", @@ -392,7 +392,7 @@ impl Topics { } } } - None => Err(CallError::ArgumentError(pos, format!("Unknown help topic {}", name))), + None => Err(CallError::SyntaxError(pos, format!("Unknown help topic {}", name))), } } diff --git a/std/src/numerics.rs b/std/src/numerics.rs index 269dfe22..9503fc7e 100644 --- a/std/src/numerics.rs +++ b/std/src/numerics.rs @@ -191,8 +191,7 @@ impl Callable for CintFunction { debug_assert_eq!(1, scope.nargs()); let (value, pos) = scope.pop_double_with_pos(); - let i = - double_to_integer(value).map_err(|e| CallError::ArgumentError(pos, e.to_string()))?; + let i = double_to_integer(value).map_err(|e| CallError::SyntaxError(pos, e.to_string()))?; scope.return_integer(i) } } @@ -323,7 +322,7 @@ impl Callable for IntFunction { let (value, pos) = scope.pop_double_with_pos(); let i = double_to_integer(value.floor()) - .map_err(|e| CallError::ArgumentError(pos, e.to_string()))?; + .map_err(|e| CallError::SyntaxError(pos, e.to_string()))?; scope.return_integer(i) } } @@ -598,7 +597,7 @@ impl Callable for RndFunction { Ordering::Equal => scope.return_double(self.prng.borrow_mut().last()), Ordering::Greater => scope.return_double(self.prng.borrow_mut().next()), Ordering::Less => { - Err(CallError::ArgumentError(npos, "n% cannot be negative".to_owned())) + Err(CallError::SyntaxError(npos, "n% cannot be negative".to_owned())) } } } @@ -687,7 +686,7 @@ impl Callable for SqrFunction { let (num, numpos) = scope.pop_double_with_pos(); if num < 0.0 { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( numpos, "Cannot take square root of a negative number".to_owned(), )); diff --git a/std/src/strings.rs b/std/src/strings.rs index bf6e9d23..70c87a65 100644 --- a/std/src/strings.rs +++ b/std/src/strings.rs @@ -132,14 +132,14 @@ impl Callable for AscFunction { let ch = match chars.next() { Some(ch) => ch, None => { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( spos, format!("Input string \"{}\" must be 1-character long", s), )); } }; if chars.next().is_some() { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( spos, format!("Input string \"{}\" must be 1-character long", s), )); @@ -196,7 +196,7 @@ impl Callable for ChrFunction { let (i, ipos) = scope.pop_integer_with_pos(); if i < 0 { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( ipos, format!("Character code {} must be positive", i), )); @@ -205,7 +205,7 @@ impl Callable for ChrFunction { match char::from_u32(code) { Some(ch) => scope.return_string(format!("{}", ch)), - None => Err(CallError::ArgumentError(ipos, format!("Invalid character code {}", code))), + None => Err(CallError::SyntaxError(ipos, format!("Invalid character code {}", code))), } } } @@ -263,7 +263,7 @@ impl Callable for LeftFunction { let (n, npos) = scope.pop_integer_with_pos(); if n < 0 { - Err(CallError::ArgumentError(npos, "n% cannot be negative".to_owned())) + Err(CallError::SyntaxError(npos, "n% cannot be negative".to_owned())) } else { let n = min(s.len(), n as usize); scope.return_string(s[..n].to_owned()) @@ -437,13 +437,13 @@ impl Callable for MidFunction { debug_assert_eq!(0, scope.nargs()); if start < 0 { - return Err(CallError::ArgumentError(startpos, "start% cannot be negative".to_owned())); + return Err(CallError::SyntaxError(startpos, "start% cannot be negative".to_owned())); } let start = min(s.len(), start as usize); let end = if let Some((length, lengthpos)) = lengtharg { if length < 0 { - return Err(CallError::ArgumentError( + return Err(CallError::SyntaxError( lengthpos, "length% cannot be negative".to_owned(), )); @@ -510,7 +510,7 @@ impl Callable for RightFunction { let (n, npos) = scope.pop_integer_with_pos(); if n < 0 { - Err(CallError::ArgumentError(npos, "n% cannot be negative".to_owned())) + Err(CallError::SyntaxError(npos, "n% cannot be negative".to_owned())) } else { let n = min(s.len(), n as usize); scope.return_string(s[s.len() - n..].to_owned()) From e1f854d196050ec7d03c89da5e14e6f3748a78e0 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 9 Nov 2024 10:34:10 -0800 Subject: [PATCH 009/110] Merge Call{Error,Result} into exec::{Error,Result} --- NEWS.md | 3 +- client/src/cmds.rs | 150 +++++++++++++++++++++------------------- core/examples/dsl.rs | 17 ++--- core/src/exec.rs | 107 +++++++++++----------------- core/src/syms.rs | 85 ++++++----------------- core/src/testutils.rs | 54 ++++++++------- std/src/arrays.rs | 44 +++++------- std/src/console/cmds.rs | 96 ++++++++++++------------- std/src/data.rs | 27 ++++---- std/src/exec.rs | 38 +++++----- std/src/gfx.rs | 100 +++++++++++---------------- std/src/gpio/mod.rs | 89 +++++++++--------------- std/src/help.rs | 72 ++++++++----------- std/src/numerics.rs | 64 ++++++++--------- std/src/program.rs | 115 +++++++++++++++--------------- std/src/storage/cmds.rs | 59 ++++++++-------- std/src/strings.rs | 94 +++++++++---------------- web/src/lib.rs | 11 ++- 18 files changed, 519 insertions(+), 706 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1699a5ff..3c2839ac 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,7 +14,8 @@ for the time being.** STILL UNDER DEVELOPMENT; NOT RELEASED YET. -* No changes recorded. +* Cleaned up internal error handling, which changes how errors returned from + commands and functions are displayed to the user. ## Changes in version 0.11.1 diff --git a/client/src/cmds.rs b/client/src/cmds.rs index 6720b027..1ddce8ac 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -21,10 +21,8 @@ use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredValueSyntax, SingularArgSyntax, }; -use endbasic_core::exec::{Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +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::storage::{FileAcls, Storage}; @@ -113,7 +111,7 @@ To create an account, use the SIGNUP command.", } /// Performs the login workflow against the server. - async fn do_login(&self, username: &str, password: &str) -> CallResult { + async fn do_login(&self, username: &str, password: &str) -> io::Result<()> { let response = self.service.borrow_mut().login(username, password).await?; { @@ -142,24 +140,22 @@ impl Callable for LoginCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { if self.service.borrow().is_logged_in() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Cannot LOGIN again before LOGOUT".to_owned(), - ) - .into()); + return Err(scope.internal_error("Cannot LOGIN again before LOGOUT")); } let username = scope.pop_string(); let password = if scope.nargs() == 0 { - read_line_secure(&mut *self.console.borrow_mut(), "Password: ").await? + read_line_secure(&mut *self.console.borrow_mut(), "Password: ") + .await + .map_err(|e| scope.io_error(e))? } else { debug_assert_eq!(1, scope.nargs()); scope.pop_string() }; - self.do_login(&username, &password).await + self.do_login(&username, &password).await.map_err(|e| scope.io_error(e)) } } @@ -201,41 +197,39 @@ impl Callable for LogoutCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); if !self.service.borrow().is_logged_in() { // TODO(jmmv): Now that the access tokens are part of the service, we can easily allow // logging in more than once within a session. Consider adding a LOGOUT command first // to make it easier to handle the CLOUD: drive on a second login. - return Err( - io::Error::new(io::ErrorKind::InvalidInput, "Must LOGIN first".to_owned()).into() - ); + return Err(scope.internal_error("Must LOGIN first")); } let unmounted = match self.storage.borrow_mut().unmount("CLOUD") { Ok(()) => true, Err(e) if e.kind() == io::ErrorKind::NotFound => false, Err(e) if e.kind() == io::ErrorKind::AlreadyExists => { - return Err(io::Error::new( - e.kind(), - "Cannot log out while the CLOUD drive is active".to_owned(), + return Err(scope.internal_error("Cannot log out while the CLOUD drive is active")); + } + Err(e) => { + return Err( + scope.io_error(io::Error::new(e.kind(), format!("Cannot log out: {}", e))) ) - .into()) } - Err(e) => return Err(io::Error::new(e.kind(), format!("Cannot log out: {}", e)).into()), }; - self.service.borrow_mut().logout().await?; + self.service.borrow_mut().logout().await.map_err(|e| scope.io_error(e))?; { let mut console = self.console.borrow_mut(); - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; if unmounted { - console.print(" Unmounted CLOUD drive")?; + console.print(" Unmounted CLOUD drive").map_err(|e| scope.io_error(e))?; } - console.print(" Good bye!")?; - console.print("")?; + console.print(" Good bye!").map_err(|e| scope.io_error(e))?; + console.print("").map_err(|e| scope.io_error(e))?; } Ok(()) @@ -311,7 +305,7 @@ impl ShareCommand { acl_pos: LineCol, add: &mut FileAcls, remove: &mut FileAcls, - ) -> Result<(), CallError> { + ) -> Result<()> { let change = if acl.len() < 3 { String::new() } else { acl.split_off(acl.len() - 2) }; let username = acl; // For clarity after splitting off the ACL change request. match (username, change.as_str()) { @@ -320,7 +314,7 @@ impl ShareCommand { (username, "-r") if !username.is_empty() => remove.add_reader(username), (username, "-R") if !username.is_empty() => remove.add_reader(username), (username, change) => { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( acl_pos, format!( "Invalid ACL '{}{}': must be of the form \"username+r\" or \"username-r\"", @@ -343,7 +337,7 @@ impl ShareCommand { } /// Fetches and prints the ACLs for `filename`. - async fn show_acls(&self, filename: &str) -> CallResult { + async fn show_acls(&self, filename: &str) -> io::Result<()> { let acls = self.storage.borrow().get_acls(filename).await?; let mut console = self.console.borrow_mut(); @@ -356,9 +350,7 @@ impl ShareCommand { console.print(&format!(" {}", acl))?; } } - console.print("")?; - - Ok(()) + console.print("") } } @@ -368,7 +360,7 @@ impl Callable for ShareCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_ne!(0, scope.nargs()); let filename = scope.pop_string(); @@ -380,10 +372,14 @@ impl Callable for ShareCommand { } if add.is_empty() && remove.is_empty() { - return self.show_acls(&filename).await; + return self.show_acls(&filename).await.map_err(|e| scope.io_error(e)); } - self.storage.borrow_mut().update_acls(&filename, &add, &remove).await?; + self.storage + .borrow_mut() + .update_acls(&filename, &add, &remove) + .await + .map_err(|e| scope.io_error(e))?; if Self::has_public_acl(&add) { let filename = match filename.split_once('/') { @@ -392,7 +388,7 @@ impl Callable for ShareCommand { }; let mut console = self.console.borrow_mut(); - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; refill_and_print( &mut *console, [ @@ -409,8 +405,9 @@ auto-run your public file by visiting:", ), ], " ", - )?; - console.print("")?; + ) + .map_err(|e| scope.io_error(e))?; + console.print("").map_err(|e| scope.io_error(e))?; } Ok(()) @@ -418,7 +415,7 @@ auto-run your public file by visiting:", } /// Checks if a password is sufficiently complex and returns an error when it isn't. -fn validate_password_complexity(password: &str) -> Result<(), &'static str> { +fn validate_password_complexity(password: &str) -> std::result::Result<(), &'static str> { if password.len() < 8 { return Err("Must be at least 8 characters long"); } @@ -510,24 +507,25 @@ impl Callable for SignupCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); let console = &mut *self.console.borrow_mut(); - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; refill_and_print( console, ["Let's gather some information to create your cloud account.", "You can abort this process at any time by hitting Ctrl+C and you will be given a chance to \ review your inputs before creating the account."], " ", - )?; - console.print("")?; + ).map_err(|e| scope.io_error(e))?; + console.print("").map_err(|e| scope.io_error(e))?; - let username = read_line(console, "Username: ", "", None).await?; - let password = Self::read_password(console).await?; + let username = + read_line(console, "Username: ", "", None).await.map_err(|e| scope.io_error(e))?; + let password = Self::read_password(console).await.map_err(|e| scope.io_error(e))?; - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; refill_and_print( console, [ @@ -538,28 +536,34 @@ promotional email messages (like new release announcements) or not, and your sel have no adverse impact in the service you receive.", ], " ", - )?; - console.print("")?; + ) + .map_err(|e| scope.io_error(e))?; + console.print("").map_err(|e| scope.io_error(e))?; - let email = read_line(console, "Email address: ", "", None).await?; + let email = + read_line(console, "Email address: ", "", None).await.map_err(|e| scope.io_error(e))?; let promotional_email = - Self::read_bool(console, "Receive promotional email (y/N)? ", false).await?; + Self::read_bool(console, "Receive promotional email (y/N)? ", false) + .await + .map_err(|e| scope.io_error(e))?; - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; refill_and_print( console, ["We are ready to go. Please review your answers before proceeding."], " ", - )?; - console.print("")?; - - console.print(&format!("Username: {}", username))?; - console.print(&format!("Email address: {}", email))?; - console.print(&format!( - "Promotional email: {}", - if promotional_email { "yes" } else { "no" } - ))?; - let proceed = Self::read_bool(console, "Continue (y/N)? ", false).await?; + ) + .map_err(|e| scope.io_error(e))?; + console.print("").map_err(|e| scope.io_error(e))?; + + console.print(&format!("Username: {}", username)).map_err(|e| scope.io_error(e))?; + console.print(&format!("Email address: {}", email)).map_err(|e| scope.io_error(e))?; + console + .print(&format!("Promotional email: {}", if promotional_email { "yes" } else { "no" })) + .map_err(|e| scope.io_error(e))?; + let proceed = Self::read_bool(console, "Continue (y/N)? ", false) + .await + .map_err(|e| scope.io_error(e))?; if !proceed { // TODO(jmmv): This should return an error of some form once we have error handling in // the language. @@ -567,9 +571,9 @@ have no adverse impact in the service you receive.", } let request = SignupRequest { username, password, email, promotional_email }; - self.service.borrow_mut().signup(&request).await?; + self.service.borrow_mut().signup(&request).await.map_err(|e| scope.io_error(e))?; - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; refill_and_print( console, ["Your account has been created and is pending activation.", @@ -578,8 +582,8 @@ in it to activate your account. Make sure to check your spam folder.", "Once your account is activated, come back here and use LOGIN to get started!", "If you encounter any problems, please contact support@endbasic.dev."], " ", - )?; - console.print("")?; + ).map_err(|e| scope.io_error(e))?; + console.print("").map_err(|e| scope.io_error(e))?; Ok(()) } @@ -715,7 +719,7 @@ mod tests { Err(io::Error::new(io::ErrorKind::PermissionDenied, "Unknown user")), ); t.run(format!(r#"LOGIN "{}", "{}""#, "bad-user", "the-password")) - .expect_err("1:1: In call to LOGIN: Unknown user") + .expect_err("1:1: Unknown user") .check(); t.get_service().borrow_mut().add_mock_login( "the-username", @@ -723,7 +727,7 @@ mod tests { Err(io::Error::new(io::ErrorKind::PermissionDenied, "Invalid password")), ); t.run(format!(r#"LOGIN "{}", "{}""#, "the-username", "bad-password")) - .expect_err("1:1: In call to LOGIN: Invalid password") + .expect_err("1:1: Invalid password") .check(); assert!(!t.get_storage().borrow().mounted().contains_key("CLOUD")); } @@ -739,7 +743,7 @@ mod tests { assert!(!t.get_storage().borrow().mounted().contains_key("CLOUD")); t.run(r#"LOGIN "the-username", "the-password": LOGIN "a", "b""#) .expect_access_token("random token") - .expect_err("1:39: In call to LOGIN: Cannot LOGIN again before LOGOUT") + .expect_err("1:39: Cannot LOGIN again before LOGOUT") .check(); assert!(t.get_storage().borrow().mounted().contains_key("CLOUD")); } @@ -799,7 +803,7 @@ mod tests { t.get_storage().borrow_mut().mount("CLOUD", "memory://").unwrap(); t.get_storage().borrow_mut().cd("CLOUD:/").unwrap(); t.run(r#"LOGOUT"#) - .expect_err("1:1: In call to LOGOUT: Cannot log out while the CLOUD drive is active") + .expect_err("1:1: Cannot log out while the CLOUD drive is active") .expect_access_token("$") .check(); assert!(t.get_storage().borrow().mounted().contains_key("CLOUD")); @@ -808,7 +812,7 @@ mod tests { #[test] fn test_logout_errors() { client_check_stmt_compilation_err("1:1: LOGOUT expected no arguments", r#"LOGOUT "a""#); - client_check_stmt_err("1:1: In call to LOGOUT: Must LOGIN first", r#"LOGOUT"#); + client_check_stmt_err("1:1: Must LOGIN first", r#"LOGOUT"#); } #[test] @@ -967,7 +971,7 @@ mod tests { r#"SHARE "a", 3, "b""#, ); client_check_stmt_err( - r#"1:1: In call to SHARE: 1:12: Invalid ACL 'foobar': must be of the form "username+r" or "username-r""#, + r#"1:12: Invalid ACL 'foobar': must be of the form "username+r" or "username-r""#, r#"SHARE "a", "foobar""#, ); } @@ -1135,7 +1139,7 @@ mod tests { .add_input_chars("true\n"); // Confirmation. let mut c = t.run("SIGNUP".to_owned()); let output = flatten_output(c.take_captured_out()); - c.expect_err("1:1: In call to SIGNUP: Some error").check(); + c.expect_err("1:1: Some error").check(); assert!(output.contains("Username: the-username")); assert!(output.contains("Email address: some@example.com")); diff --git a/core/examples/dsl.rs b/core/examples/dsl.rs index 1558425d..85a99cb4 100644 --- a/core/examples/dsl.rs +++ b/core/examples/dsl.rs @@ -24,10 +24,8 @@ use async_trait::async_trait; use endbasic_core::ast::ExprType; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; -use endbasic_core::exec::{Machine, Scope, StopReason}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +use endbasic_core::exec::{Error, Machine, Result, Scope, StopReason}; +use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use futures_lite::future::block_on; use std::borrow::Cow; use std::cell::RefCell; @@ -77,7 +75,7 @@ impl Callable for NumLightsFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); let num = self.lights.borrow().len(); assert!(num <= i32::MAX as usize, "Ended up with too many lights"); @@ -117,20 +115,17 @@ impl Callable for SwitchLightCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (i, ipos) = scope.pop_integer_with_pos(); let lights = &mut *self.lights.borrow_mut(); if i < 1 { - return Err(CallError::SyntaxError( - ipos, - "Light id cannot be zero or negative".to_owned(), - )); + return Err(Error::SyntaxError(ipos, "Light id cannot be zero or negative".to_owned())); } let i = i as usize; if i > lights.len() { - return Err(CallError::SyntaxError(ipos, "Light id out of range".to_owned())); + return Err(Error::SyntaxError(ipos, "Light id out of range".to_owned())); } if lights[i - 1] { println!("Turning light {} off", i); diff --git a/core/src/exec.rs b/core/src/exec.rs index 99f94d3a..ca3564cd 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -20,9 +20,7 @@ use crate::bytecode::*; use crate::compiler; use crate::parser; use crate::reader::LineCol; -use crate::syms::CallResult; -use crate::syms::SymbolKey; -use crate::syms::{CallError, Callable, CallableMetadata, Symbol, Symbols}; +use crate::syms::{Callable, Symbol, SymbolKey, Symbols}; use crate::value; use crate::value::double_to_integer; use async_channel::{Receiver, Sender, TryRecvError}; @@ -39,9 +37,13 @@ pub enum Error { CompilerError(#[from] compiler::Error), /// Evaluation error during execution. - #[error("{}: {}", .0, .1)] + #[error("{0}: {1}")] EvalError(LineCol, String), + /// Any other error not representable by other values. + #[error("{0}: {1}")] + InternalError(LineCol, String), + /// I/O error during execution. #[error("{0}: {1}")] IoError(LineCol, io::Error), @@ -51,38 +53,11 @@ pub enum Error { ParseError(#[from] parser::Error), /// Syntax error. - #[error("{}: {}", .0, .1)] + #[error("{0}: {1}")] SyntaxError(LineCol, String), } impl Error { - /// Annotates a call evaluation error with the command's metadata. - // TODO(jmmv): This is a hack to support the transition to a better Command abstraction within - // Symbols and exists to minimize the amount of impacted tests. Should be removed and/or - // somehow unified with the equivalent function in eval::Error. - fn from_call_error(md: &CallableMetadata, e: CallError, pos: LineCol) -> Self { - match e { - CallError::EvalError(pos2, e) => { - if !md.is_function() { - Self::EvalError(pos2, e) - } else { - Self::EvalError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } - } - CallError::InternalError(pos2, e) => { - Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } - CallError::IoError(e) => Self::IoError( - pos, - io::Error::new(e.kind(), format!("In call to {}: {}", md.name(), e)), - ), - CallError::NestedError(e) => e, - CallError::SyntaxError(pos2, e) => { - Self::SyntaxError(pos, format!("In call to {}: {}: {}", md.name(), pos2, e)) - } - } - } - /// Annotates a value computation error with a position. fn from_value_error(e: value::Error, pos: LineCol) -> Self { Self::EvalError(pos, e.message) @@ -93,6 +68,7 @@ impl Error { match self { Error::CompilerError(_) => false, Error::EvalError(..) => true, + Error::InternalError(..) => true, Error::IoError(..) => true, Error::ParseError(_) => false, Error::SyntaxError(..) => true, @@ -360,11 +336,22 @@ impl<'s> Scope<'s> { Self { stack, nargs, fref_pos } } + /// Removes all remaining arguments from the stack tracked by this scope. fn drain(&mut self) { self.stack.discard(self.nargs); self.nargs = 0; } + /// Annotates an I/O error with the position of the callable that generated it. + pub fn io_error(&self, e: io::Error) -> Error { + Error::IoError(self.fref_pos, e) + } + + /// Creates an internal error with the position of the callable that generated it. + pub fn internal_error>(&self, msg: S) -> Error { + Error::InternalError(self.fref_pos, msg.into()) + } + /// Returns the number of arguments that can still be consumed. pub fn nargs(&self) -> usize { self.nargs @@ -481,35 +468,35 @@ impl<'s> Scope<'s> { } /// Sets the return value of this function to `value`. - pub fn return_any(mut self, value: Value) -> CallResult { + pub fn return_any(mut self, value: Value) -> Result<()> { self.drain(); self.stack.push((value, self.fref_pos)); Ok(()) } /// Sets the return value of this function to the boolean `value`. - pub fn return_boolean(mut self, value: bool) -> CallResult { + pub fn return_boolean(mut self, value: bool) -> Result<()> { self.drain(); self.stack.push((Value::Boolean(value), self.fref_pos)); Ok(()) } /// Sets the return value of this function to the double `value`. - pub fn return_double(mut self, value: f64) -> CallResult { + pub fn return_double(mut self, value: f64) -> Result<()> { self.drain(); self.stack.push((Value::Double(value), self.fref_pos)); Ok(()) } /// Sets the return value of this function to the integer `value`. - pub fn return_integer(mut self, value: i32) -> CallResult { + pub fn return_integer(mut self, value: i32) -> Result<()> { self.drain(); self.stack.push((Value::Integer(value), self.fref_pos)); Ok(()) } /// Sets the return value of this function to the string `value`. - pub fn return_string>(mut self, value: S) -> CallResult { + pub fn return_string>(mut self, value: S) -> Result<()> { self.drain(); self.stack.push((Value::Text(value.into()), self.fref_pos)); Ok(()) @@ -688,8 +675,7 @@ impl Machine { let scope = Scope::new(&mut context.value_stack, nargs, bref_pos); let b = b.clone(); - b.exec(scope, self).await.map_err(|e| Error::from_call_error(b.metadata(), e, bref_pos))?; - Ok(()) + b.exec(scope, self).await } /// Handles an array definition. The array must not yet exist, and the name may not overlap @@ -902,7 +888,7 @@ impl Machine { debug_assert_eq!(return_type, metadata.return_type().unwrap()); let scope = Scope::new(&mut context.value_stack, nargs, fref_pos); - f.exec(scope, self).await.map_err(|e| Error::from_call_error(metadata, e, fref_pos))?; + f.exec(scope, self).await?; if cfg!(debug_assertions) { match context.value_stack.top() { Some((value, _pos)) => { @@ -974,9 +960,8 @@ impl Machine { fpos: LineCol, f: Rc, ) -> Result<()> { - let metadata = f.metadata(); let scope = Scope::new(&mut context.value_stack, 0, fpos); - f.exec(scope, self).await.map_err(|e| Error::from_call_error(metadata, e, fpos))?; + f.exec(scope, self).await?; if cfg!(debug_assertions) { match context.value_stack.top() { Some((value, _pos)) => { @@ -2782,7 +2767,7 @@ mod tests { 100 OUT LAST_ERROR "#, &[], - &["1", "4:17: In call to RAISEF: 4:24: Some internal error"], + &["1", "4:24: Some internal error"], ); } @@ -2798,7 +2783,7 @@ mod tests { OUT LAST_ERROR "#, &[], - &["1", "4:17: In call to RAISEF: 4:24: Some internal error"], + &["1", "4:24: Some internal error"], ); } @@ -2816,7 +2801,7 @@ mod tests { "#, &[], &["1", "2"], - "8:17: In call to RAISEF: 8:24: Some internal error", + "8:24: Some internal error", ); } @@ -2830,7 +2815,7 @@ mod tests { OUT LAST_ERROR "#, &[], - &["1", "4:17: In call to RAISEF: 4:24: Some internal error"], + &["1", "4:24: Some internal error"], ); } @@ -2844,7 +2829,7 @@ mod tests { OUT LAST_ERROR "#, &[], - &["1", "4:13: In call to RAISE: 4:19: Some internal error"], + &["1", "4:19: Some internal error"], ); } @@ -2856,7 +2841,7 @@ mod tests { OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR "#, &[], - &["1", "3:24: In call to RAISEF: 3:31: Some internal error"], + &["1", "3:31: Some internal error"], ); } @@ -2868,7 +2853,7 @@ mod tests { OUT 1: RAISE "internal": OUT LAST_ERROR "#, &[], - &["1", "3:20: In call to RAISE: 3:26: Some internal error"], + &["1", "3:26: Some internal error"], ); } @@ -2877,25 +2862,25 @@ mod tests { do_ok_test( r#"ON ERROR RESUME NEXT: OUT RAISEF("argument"): OUT LAST_ERROR"#, &[], - &["1:27: In call to RAISEF: 1:34: Bad argument"], + &["1:34: Bad argument"], ); do_ok_test( r#"ON ERROR RESUME NEXT: OUT RAISEF("eval"): OUT LAST_ERROR"#, &[], - &["1:27: In call to RAISEF: 1:34: Some eval error"], + &["1:34: Some eval error"], ); do_ok_test( r#"ON ERROR RESUME NEXT: OUT RAISEF("internal"): OUT LAST_ERROR"#, &[], - &["1:27: In call to RAISEF: 1:34: Some internal error"], + &["1:34: Some internal error"], ); do_ok_test( r#"ON ERROR RESUME NEXT: OUT RAISEF("io"): OUT LAST_ERROR"#, &[], - &["1:27: In call to RAISEF: Some I/O error"], + &["1:34: Some I/O error"], ); } @@ -3165,13 +3150,8 @@ mod tests { #[test] fn test_top_level_semantic_errors_allow_execution() { - do_simple_error_test(r#"OUT RAISEF("io")"#, "1:5: In call to RAISEF: Some I/O error"); - do_error_test( - r#"OUT "a": OUT RAISEF("io"): OUT "b""#, - &[], - &["a"], - "1:14: In call to RAISEF: Some I/O error", - ); + do_simple_error_test(r#"OUT RAISEF("io")"#, "1:12: Some I/O error"); + do_error_test(r#"OUT "a": OUT RAISEF("io"): OUT "b""#, &[], &["a"], "1:21: Some I/O error"); } #[test] @@ -3182,15 +3162,12 @@ mod tests { #[test] fn test_inner_level_semantic_errors_allow_execution() { - do_simple_error_test( - r#"IF TRUE THEN: OUT RAISEF("io"): END IF"#, - "1:19: In call to RAISEF: Some I/O error", - ); + do_simple_error_test(r#"IF TRUE THEN: OUT RAISEF("io"): END IF"#, "1:26: Some I/O error"); do_error_test( r#"OUT "a": IF TRUE THEN: OUT RAISEF("io"): END IF: OUT "b""#, &[], &["a"], - "1:28: In call to RAISEF: Some I/O error", + "1:35: Some I/O error", ); } diff --git a/core/src/syms.rs b/core/src/syms.rs index 67be8b72..f24543b5 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -16,58 +16,17 @@ //! Symbol definitions and symbols table representation. use crate::ast::{ExprType, Value, VarRef}; -use crate::compiler::{self, CallableSyntax, RepeatedSyntax, SingularArgSyntax}; +use crate::compiler::{CallableSyntax, RepeatedSyntax, SingularArgSyntax}; use crate::exec::{self, Machine, Scope}; -use crate::reader::LineCol; -use crate::value::{Error, Result}; +use crate::value; use async_trait::async_trait; use std::borrow::Cow; use std::collections::HashMap; use std::fmt; -use std::io; use std::mem; use std::rc::Rc; use std::str::Lines; -/// Command or function execution errors. -/// -/// These are separate from the more generic `Error` type because they are not annotated with the -/// specific callable that triggered the error. We add such annotation once we capture the error -/// within the evaluation logic. -#[derive(Debug)] -pub enum CallError { - /// Error while evaluating input arguments. - EvalError(LineCol, String), - - /// Any other error not representable by other values. - InternalError(LineCol, String), - - /// I/O error during execution. - IoError(io::Error), - - /// Hack to support errors that arise from within a program that is `RUN`. - // TODO(jmmv): Consider unifying `CallError` with `exec::Error`. - NestedError(exec::Error), - - /// A specific parameter had an invalid value. - SyntaxError(LineCol, String), -} - -impl From for CallError { - fn from(e: io::Error) -> Self { - Self::IoError(e) - } -} - -impl From for CallError { - fn from(value: compiler::Error) -> Self { - Self::NestedError(exec::Error::CompilerError(value)) - } -} - -/// Result for callable execution return values. -pub type CallResult = std::result::Result<(), CallError>; - /// The key of a symbol in the symbols table. #[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] pub struct SymbolKey(String); @@ -126,11 +85,11 @@ impl Array { } /// Validates that the subscript `i` is in the `[0,max)` range and converts it to an `usize`. - fn validate_subscript(i: i32, max: usize) -> Result { + fn validate_subscript(i: i32, max: usize) -> value::Result { if i < 0 { - Err(Error::new(format!("Subscript {} cannot be negative", i))) + Err(value::Error::new(format!("Subscript {} cannot be negative", i))) } else if (i as usize) >= max { - Err(Error::new(format!("Subscript {} exceeds limit of {}", i, max))) + Err(value::Error::new(format!("Subscript {} exceeds limit of {}", i, max))) } else { Ok(i as usize) } @@ -140,7 +99,7 @@ impl Array { /// /// It is an error if `dimensions` and `subscripts` have different sizes, or if the values in /// `subscripts` are negative. - fn native_index(dimensions: &[usize], subscripts: &[i32]) -> Result { + fn native_index(dimensions: &[usize], subscripts: &[i32]) -> value::Result { debug_assert_eq!( subscripts.len(), dimensions.len(), @@ -161,7 +120,7 @@ impl Array { } /// Assings the `value` to the array position indicated by the `subscripts`. - pub fn assign(&mut self, subscripts: &[i32], value: Value) -> Result<()> { + pub fn assign(&mut self, subscripts: &[i32], value: Value) -> value::Result<()> { debug_assert_eq!( subscripts.len(), self.dimensions.len(), @@ -180,7 +139,7 @@ impl Array { } /// Obtains the value contained in the array position indicated by the `subscripts`. - pub fn index(&self, subscripts: &[i32]) -> Result<&Value> { + pub fn index(&self, subscripts: &[i32]) -> value::Result<&Value> { let i = Array::native_index(&self.dimensions, subscripts)?; let value = &self.values[i]; debug_assert!(value.as_exprtype() == self.subtype); @@ -393,13 +352,13 @@ impl Symbols { /// Obtains the value of a symbol or `None` if it is not defined. /// /// Returns an error if the type annotation in the symbol reference does not match its type. - pub fn get(&self, vref: &VarRef) -> Result> { + pub fn get(&self, vref: &VarRef) -> value::Result> { let key = SymbolKey::from(vref.name()); let symbol = self.load(&key); if let Some(symbol) = symbol { let stype = symbol.eval_type(); if !vref.accepts_callable(stype) { - return Err(Error::new(format!( + return Err(value::Error::new(format!( "Incompatible type annotation in {} reference", vref ))); @@ -417,12 +376,12 @@ impl Symbols { /// Obtains the value of a symbol or `None` if it is not defined. /// /// Returns an error if the type annotation in the symbol reference does not match its type. - pub fn get_mut(&mut self, vref: &VarRef) -> Result> { + pub fn get_mut(&mut self, vref: &VarRef) -> value::Result> { match self.load_mut(&vref.as_symbol_key()) { Some(symbol) => { let stype = symbol.eval_type(); if !vref.accepts_callable(stype) { - return Err(Error::new(format!( + return Err(value::Error::new(format!( "Incompatible type annotation in {} reference", vref ))); @@ -439,11 +398,11 @@ impl Symbols { /// or if the type annotation in the variable reference does not match the type of the value /// that the variable contains. #[cfg(test)] - pub(crate) fn get_var(&self, vref: &VarRef) -> Result<&Value> { + pub(crate) fn get_var(&self, vref: &VarRef) -> value::Result<&Value> { match self.get(vref)? { Some(Symbol::Variable(v)) => Ok(v), - Some(_) => Err(Error::new(format!("{} is not a variable", vref.name()))), - None => Err(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()))), } } @@ -480,14 +439,14 @@ 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) -> Result<()> { + 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())?; match self.get_mut(vref)? { Some(Symbol::Variable(old_value)) => { let value = value.maybe_cast(Some(old_value.as_exprtype()))?; if mem::discriminant(&value) != mem::discriminant(old_value) { - return Err(Error::new(format!( + return Err(value::Error::new(format!( "Cannot assign value of type {} to variable of type {}", value.as_exprtype(), old_value.as_exprtype(), @@ -496,11 +455,11 @@ impl Symbols { self.assign(&key, value); Ok(()) } - Some(_) => Err(Error::new(format!("Cannot redefine {} as a variable", vref))), + 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(Error::new(format!( + return Err(value::Error::new(format!( "Cannot assign value of type {} to variable of type {}", value.as_exprtype(), ref_type, @@ -514,10 +473,10 @@ impl Symbols { } /// Unsets the symbol `key` irrespective of its type. - pub(crate) fn unset(&mut self, key: &SymbolKey) -> Result<()> { + pub(crate) fn unset(&mut self, key: &SymbolKey) -> value::Result<()> { match self.scopes.last_mut().unwrap().remove(key) { Some(_) => Ok(()), - None => Err(Error::new(format!("{} is not defined", key))), + None => Err(value::Error::new(format!("{} is not defined", key))), } } } @@ -740,7 +699,7 @@ pub trait Callable { /// `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<'_>, machine: &mut Machine) -> CallResult; + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> exec::Result<()>; } #[cfg(test)] diff --git a/core/src/testutils.rs b/core/src/testutils.rs index 6bb9ea5c..ab64292c 100644 --- a/core/src/testutils.rs +++ b/core/src/testutils.rs @@ -20,10 +20,9 @@ use crate::compiler::{ ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax, RequiredValueSyntax, SingularArgSyntax, }; -use crate::exec::{Machine, Scope, ValueTag}; +use crate::exec::{Error, Machine, Result, Scope, ValueTag}; use crate::syms::{ - Array, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbol, - SymbolKey, Symbols, + Array, Callable, CallableMetadata, CallableMetadataBuilder, Symbol, SymbolKey, Symbols, }; use crate::value; use async_trait::async_trait; @@ -57,7 +56,7 @@ impl Callable for ArglessFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { assert_eq!(0, scope.nargs()); scope.return_any(self.value.clone()) } @@ -84,7 +83,7 @@ impl Callable for ClearCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { assert_eq!(0, scope.nargs()); machine.clear(); Ok(()) @@ -115,7 +114,7 @@ impl Callable for CountFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { assert_eq!(0, scope.nargs()); let mut counter = self.counter.borrow_mut(); *counter += 1; @@ -152,14 +151,17 @@ impl Callable for RaisefFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { assert_eq!(1, scope.nargs()); let (arg, pos) = scope.pop_string_with_pos(); match arg.as_str() { - "argument" => Err(CallError::SyntaxError(pos, "Bad argument".to_owned())), - "eval" => Err(CallError::EvalError(pos, "Some eval error".to_owned())), - "internal" => Err(CallError::InternalError(pos, "Some internal error".to_owned())), - "io" => Err(io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()).into()), + "argument" => Err(Error::SyntaxError(pos, "Bad argument".to_owned())), + "eval" => Err(Error::EvalError(pos, "Some eval error".to_owned())), + "internal" => Err(Error::InternalError(pos, "Some internal error".to_owned())), + "io" => Err(Error::IoError( + pos, + io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()), + )), _ => panic!("Invalid arguments"), } } @@ -192,14 +194,17 @@ impl Callable for RaiseCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { assert_eq!(1, scope.nargs()); let (arg, pos) = scope.pop_string_with_pos(); match arg.as_str() { - "argument" => Err(CallError::SyntaxError(pos, "Bad argument".to_owned())), - "eval" => Err(CallError::EvalError(pos, "Some eval error".to_owned())), - "internal" => Err(CallError::InternalError(pos, "Some internal error".to_owned())), - "io" => Err(io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()).into()), + "argument" => Err(Error::SyntaxError(pos, "Bad argument".to_owned())), + "eval" => Err(Error::EvalError(pos, "Some eval error".to_owned())), + "internal" => Err(Error::InternalError(pos, "Some internal error".to_owned())), + "io" => Err(Error::IoError( + pos, + io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()), + )), _ => panic!("Invalid arguments"), } } @@ -227,7 +232,7 @@ impl Callable for LastErrorFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { assert_eq!(0, scope.nargs()); match machine.last_error() { Some(message) => scope.return_string(message), @@ -260,7 +265,7 @@ impl Callable for GetDataCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { assert_eq!(0, scope.nargs()); *self.data.borrow_mut() = machine.get_data().to_vec(); Ok(()) @@ -304,7 +309,7 @@ impl Callable for InCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (vname, vtype) = scope.pop_varref(); @@ -357,7 +362,7 @@ impl Callable for OutCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let mut first = true; let mut text = String::new(); while scope.nargs() > 0 { @@ -432,7 +437,7 @@ impl Callable for OutfFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { assert_ne!(0, scope.nargs()); let result = scope.pop_integer(); @@ -485,12 +490,11 @@ impl Callable for SumFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let mut result = 0; while scope.nargs() > 0 { let (i, pos) = scope.pop_integer_with_pos(); - result = - value::add_integer(result, i).map_err(|e| CallError::EvalError(pos, e.message))?; + result = value::add_integer(result, i).map_err(|e| Error::EvalError(pos, e.message))?; } scope.return_integer(result) } @@ -567,7 +571,7 @@ impl Callable for TypeCheckFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { assert_eq!(0, scope.nargs()); scope.return_any(self.value.clone()) } diff --git a/std/src/arrays.rs b/std/src/arrays.rs index 29df4685..60df1641 100644 --- a/std/src/arrays.rs +++ b/std/src/arrays.rs @@ -20,10 +20,9 @@ use endbasic_core::ast::{ArgSep, ExprType, VarRef}; use endbasic_core::compiler::{ ArgSepSyntax, RequiredRefSyntax, RequiredValueSyntax, SingularArgSyntax, }; -use endbasic_core::exec::{Machine, Scope}; +use endbasic_core::exec::{Error, Machine, Result, Scope}; use endbasic_core::syms::{ - Array, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbol, - Symbols, + Array, Callable, CallableMetadata, CallableMetadataBuilder, Symbol, Symbols, }; use std::borrow::Cow; use std::rc::Rc; @@ -34,31 +33,26 @@ const CATEGORY: &str = "Array functions"; /// Extracts the array reference and the dimension number from the list of arguments passed to /// either `LBOUND` or `UBOUND`. #[allow(clippy::needless_lifetimes)] -fn parse_bound_args<'a>( - scope: &mut Scope<'_>, - symbols: &'a Symbols, -) -> Result<(&'a Array, usize), CallError> { +fn parse_bound_args<'a>(scope: &mut Scope<'_>, symbols: &'a Symbols) -> Result<(&'a Array, usize)> { let (arrayname, arraytype, arraypos) = scope.pop_varref_with_pos(); let arrayref = VarRef::new(arrayname.to_string(), Some(arraytype)); - let array = match symbols - .get(&arrayref) - .map_err(|e| CallError::SyntaxError(arraypos, format!("{}", e)))? - { - Some(Symbol::Array(array)) => array, - _ => unreachable!(), - }; + let array = + match symbols.get(&arrayref).map_err(|e| Error::SyntaxError(arraypos, format!("{}", e)))? { + Some(Symbol::Array(array)) => array, + _ => unreachable!(), + }; if scope.nargs() == 1 { let (i, pos) = scope.pop_integer_with_pos(); if i < 0 { - return Err(CallError::SyntaxError(pos, format!("Dimension {} must be positive", i))); + return Err(Error::SyntaxError(pos, format!("Dimension {} must be positive", i))); } let i = i as usize; if i > array.dimensions().len() { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( pos, format!( "Array {} has only {} dimensions but asked for {}", @@ -73,7 +67,7 @@ fn parse_bound_args<'a>( debug_assert_eq!(0, scope.nargs()); if array.dimensions().len() > 1 { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( arraypos, "Requires a dimension for multidimensional arrays".to_owned(), )); @@ -146,7 +140,7 @@ impl Callable for LboundFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { let (_array, _dim) = parse_bound_args(&mut scope, machine.get_symbols())?; scope.return_integer(0) } @@ -215,7 +209,7 @@ impl Callable for UboundFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { let (array, dim) = parse_bound_args(&mut scope, machine.get_symbols())?; scope.return_integer((array.dimensions()[dim - 1] - 1) as i32) } @@ -252,7 +246,7 @@ mod tests { Tester::default() .run(format!("DIM x(2): result = {}(x, -1)", func)) - .expect_err(format!("1:20: In call to {}: 1:30: Dimension -1 must be positive", func)) + .expect_err("1:30: Dimension -1 must be positive") .expect_array("x", ExprType::Integer, &[2], vec![]) .check(); @@ -288,19 +282,13 @@ mod tests { Tester::default() .run(format!("DIM x(2, 3, 4): result = {}(x)", func)) - .expect_err(format!( - "1:26: In call to {}: 1:33: Requires a dimension for multidimensional arrays", - func - )) + .expect_err("1:33: Requires a dimension for multidimensional arrays") .expect_array("x", ExprType::Integer, &[2, 3, 4], vec![]) .check(); Tester::default() .run(format!("DIM x(2, 3, 4): result = {}(x, 5)", func)) - .expect_err(format!( - "1:26: In call to {}: 1:36: Array X has only 3 dimensions but asked for 5", - func - )) + .expect_err("1:36: Array X has only 3 dimensions but asked for 5") .expect_array("x", ExprType::Integer, &[2, 3, 4], vec![]) .check(); } diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index a08c5ece..2213b8a6 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -26,10 +26,8 @@ use endbasic_core::compiler::{ ArgSepSyntax, OptionalValueSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax, RequiredValueSyntax, SingularArgSyntax, }; -use endbasic_core::exec::{Machine, Scope, ValueTag}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +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; @@ -81,9 +79,9 @@ impl Callable for ClsCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - self.console.borrow_mut().clear(ClearType::All)?; + self.console.borrow_mut().clear(ClearType::All).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -157,16 +155,16 @@ impl Callable for ColorCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { - fn get_color((i, pos): (i32, LineCol)) -> Result, CallError> { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + fn get_color((i, pos): (i32, LineCol)) -> Result> { if i >= 0 && i <= u8::MAX as i32 { Ok(Some(i as u8)) } else { - Err(CallError::SyntaxError(pos, "Color out of range".to_owned())) + Err(Error::SyntaxError(pos, "Color out of range".to_owned())) } } - fn get_optional_color(scope: &mut Scope<'_>) -> Result, CallError> { + fn get_optional_color(scope: &mut Scope<'_>) -> Result> { match scope.pop_integer() { ColorCommand::NO_COLOR => Ok(None), ColorCommand::HAS_COLOR => get_color(scope.pop_integer_with_pos()), @@ -182,7 +180,7 @@ impl Callable for ColorCommand { (get_optional_color(&mut scope)?, get_optional_color(&mut scope)?) }; - self.console.borrow_mut().set_color(fg, bg)?; + self.console.borrow_mut().set_color(fg, bg).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -227,10 +225,10 @@ impl Callable for InKeyFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - let key = self.console.borrow_mut().poll_key().await?; + let key = self.console.borrow_mut().poll_key().await.map_err(|e| scope.io_error(e))?; let key_name = match key { Some(Key::ArrowDown) => "DOWN".to_owned(), Some(Key::ArrowLeft) => "LEFT".to_owned(), @@ -324,7 +322,7 @@ impl Callable for InputCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { let prompt = if scope.nargs() == 1 { "".to_owned() } else { @@ -362,7 +360,7 @@ impl Callable for InputCommand { machine .get_mut_symbols() .set_var(&vref, Value::Boolean(b)) - .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?; + .map_err(|e| Error::EvalError(pos, format!("{}", e)))?; return Ok(()); } Err(e) => e, @@ -373,7 +371,7 @@ impl Callable for InputCommand { machine .get_mut_symbols() .set_var(&vref, Value::Double(d)) - .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?; + .map_err(|e| Error::EvalError(pos, format!("{}", e)))?; return Ok(()); } Err(e) => e, @@ -384,7 +382,7 @@ impl Callable for InputCommand { machine .get_mut_symbols() .set_var(&vref, Value::Integer(i)) - .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?; + .map_err(|e| Error::EvalError(pos, format!("{}", e)))?; return Ok(()); } Err(e) => e, @@ -394,18 +392,18 @@ impl Callable for InputCommand { machine .get_mut_symbols() .set_var(&vref, Value::Text(trimmed_answer.to_owned())) - .map_err(|e| CallError::EvalError(pos, format!("{}", e)))?; + .map_err(|e| Error::EvalError(pos, format!("{}", e)))?; return Ok(()); } }; - console.print(&format!("Retry input: {}", e))?; + console.print(&format!("Retry input: {}", e)).map_err(|e| scope.io_error(e))?; previous_answer = answer; } Err(e) if e.kind() == io::ErrorKind::InvalidData => { - console.print(&format!("Retry input: {}", e))? + console.print(&format!("Retry input: {}", e)).map_err(|e| scope.io_error(e))? } - Err(e) => return Err(e.into()), + Err(e) => return Err(scope.io_error(e)), } } } @@ -455,11 +453,11 @@ impl Callable for LocateCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { - fn get_coord((i, pos): (i32, LineCol), name: &str) -> Result<(u16, LineCol), CallError> { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + fn get_coord((i, pos): (i32, LineCol), name: &str) -> Result<(u16, LineCol)> { match u16::try_from(i) { Ok(v) => Ok((v, pos)), - Err(_) => Err(CallError::SyntaxError(pos, format!("{} out of range", name))), + Err(_) => Err(Error::SyntaxError(pos, format!("{} out of range", name))), } } @@ -468,22 +466,22 @@ impl Callable for LocateCommand { let (row, row_pos) = get_coord(scope.pop_integer_with_pos(), "Row")?; let mut console = self.console.borrow_mut(); - let size = console.size_chars()?; + let size = console.size_chars().map_err(|e| scope.io_error(e))?; if column >= size.x { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( column_pos, format!("Column {} exceeds visible range of {}", column, size.x - 1), )); } if row >= size.y { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( row_pos, format!("Row {} exceeds visible range of {}", row, size.y - 1), )); } - console.locate(CharsXY::new(column, row))?; + console.locate(CharsXY::new(column, row)).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -534,7 +532,7 @@ impl Callable for PrintCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let mut text = String::new(); let mut nl = true; while scope.nargs() > 0 { @@ -588,9 +586,9 @@ impl Callable for PrintCommand { } if nl { - self.console.borrow_mut().print(&text)?; + self.console.borrow_mut().print(&text).map_err(|e| scope.io_error(e))?; } else { - self.console.borrow_mut().write(&text)?; + self.console.borrow_mut().write(&text).map_err(|e| scope.io_error(e))?; } Ok(()) } @@ -626,9 +624,9 @@ impl Callable for ScrColsFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - let size = self.console.borrow().size_chars()?; + let size = self.console.borrow().size_chars().map_err(|e| scope.io_error(e))?; scope.return_integer(i32::from(size.x)) } } @@ -663,9 +661,9 @@ impl Callable for ScrRowsFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - let size = self.console.borrow().size_chars()?; + let size = self.console.borrow().size_chars().map_err(|e| scope.io_error(e))?; scope.return_integer(i32::from(size.y)) } } @@ -723,8 +721,8 @@ mod tests { ); check_stmt_compilation_err("1:1: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1; 2"); - check_stmt_err("1:1: In call to COLOR: 1:7: Color out of range", "COLOR 1000, 0"); - check_stmt_err("1:1: In call to COLOR: 1:10: Color out of range", "COLOR 0, 1000"); + check_stmt_err("1:7: Color out of range", "COLOR 1000, 0"); + check_stmt_err("1:10: Color out of range", "COLOR 0, 1000"); check_stmt_compilation_err("1:7: BOOLEAN is not a number", "COLOR TRUE, 0"); check_stmt_compilation_err("1:10: BOOLEAN is not a number", "COLOR 0, TRUE"); @@ -888,30 +886,22 @@ mod tests { check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1, 2, 3"); check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1; 2"); - check_stmt_err("1:1: In call to LOCATE: 1:8: Column out of range", "LOCATE -1, 2"); - check_stmt_err("1:1: In call to LOCATE: 1:8: Column out of range", "LOCATE 70000, 2"); + check_stmt_err("1:8: Column out of range", "LOCATE -1, 2"); + check_stmt_err("1:8: Column out of range", "LOCATE 70000, 2"); check_stmt_compilation_err("1:8: BOOLEAN is not a number", "LOCATE TRUE, 2"); check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE , 2"); - check_stmt_err("1:1: In call to LOCATE: 1:11: Row out of range", "LOCATE 1, -2"); - check_stmt_err("1:1: In call to LOCATE: 1:11: Row out of range", "LOCATE 1, 70000"); + check_stmt_err("1:11: Row out of range", "LOCATE 1, -2"); + check_stmt_err("1:11: Row out of range", "LOCATE 1, 70000"); check_stmt_compilation_err("1:11: BOOLEAN is not a number", "LOCATE 1, TRUE"); check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1,"); let mut t = Tester::default(); t.get_console().borrow_mut().set_size_chars(CharsXY { x: 30, y: 20 }); - t.run("LOCATE 30, 0") - .expect_err("1:1: In call to LOCATE: 1:8: Column 30 exceeds visible range of 29") - .check(); - t.run("LOCATE 31, 0") - .expect_err("1:1: In call to LOCATE: 1:8: Column 31 exceeds visible range of 29") - .check(); - t.run("LOCATE 0, 20") - .expect_err("1:1: In call to LOCATE: 1:11: Row 20 exceeds visible range of 19") - .check(); - t.run("LOCATE 0, 21") - .expect_err("1:1: In call to LOCATE: 1:11: Row 21 exceeds visible range of 19") - .check(); + t.run("LOCATE 30, 0").expect_err("1:8: Column 30 exceeds visible range of 29").check(); + t.run("LOCATE 31, 0").expect_err("1:8: Column 31 exceeds visible range of 29").check(); + t.run("LOCATE 0, 20").expect_err("1:11: Row 20 exceeds visible range of 19").check(); + t.run("LOCATE 0, 21").expect_err("1:11: Row 21 exceeds visible range of 19").check(); } #[test] diff --git a/std/src/data.rs b/std/src/data.rs index 9f51cb12..556fc0aa 100644 --- a/std/src/data.rs +++ b/std/src/data.rs @@ -18,10 +18,8 @@ use async_trait::async_trait; use endbasic_core::ast::{ArgSep, ExprType, Value, VarRef}; use endbasic_core::compiler::{ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax}; -use endbasic_core::exec::{Clearable, Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +use endbasic_core::exec::{Clearable, Error, Machine, Result, Scope}; +use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; @@ -85,7 +83,7 @@ impl Callable for ReadCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_ne!(0, scope.nargs()); let mut vrefs = Vec::with_capacity(scope.nargs()); @@ -99,7 +97,7 @@ impl Callable for ReadCommand { let data = machine.get_data(); debug_assert!(*index <= data.len()); if *index == data.len() { - return Err(CallError::InternalError( + return Err(Error::InternalError( pos, format!("Out of data reading into {}", vname), )); @@ -119,7 +117,7 @@ impl Callable for ReadCommand { machine .get_mut_symbols() .set_var(&vref, datum) - .map_err(|e| CallError::SyntaxError(pos, format!("{}", e)))?; + .map_err(|e| Error::SyntaxError(pos, format!("{}", e)))?; } Ok(()) @@ -156,7 +154,7 @@ impl Callable for RestoreCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); *self.index.borrow_mut() = 0; Ok(()) @@ -254,7 +252,7 @@ mod tests { fn test_read_out_of_data() { Tester::default() .run(r#"DATA 5: READ i: READ j"#) - .expect_err("1:17: In call to READ: 1:22: Out of data reading into J") + .expect_err("1:22: Out of data reading into J") .expect_var("I", Value::Integer(5)) .check(); } @@ -278,7 +276,7 @@ mod tests { let mut t = Tester::default(); t.run(r#"DATA 1: READ i, j"#) .expect_var("i", Value::Integer(1)) - .expect_err("1:9: In call to READ: 1:17: Out of data reading into J") + .expect_err("1:17: Out of data reading into J") .check(); // This represents a second invocation in the REPL, which in principle should work to avoid @@ -289,7 +287,7 @@ mod tests { // extra hooks into `machine.exec()` just for this single use case seems overkill. t.run(r#"DATA 1, 2: READ i, j"#) .expect_var("i", Value::Integer(2)) - .expect_err("1:12: In call to READ: 1:20: Out of data reading into J") + .expect_err("1:20: Out of data reading into J") .check(); // Running `CLEAR` explicitly should resolve the issue described above and give us the @@ -309,12 +307,13 @@ mod tests { check_stmt_compilation_err("1:1: READ expected vref1[, .., vrefN]", "READ i; j"); check_stmt_err( - "1:11: In call to READ: 1:16: Cannot assign value of type STRING to variable of type INTEGER", + "1:16: Cannot assign value of type STRING to variable of type INTEGER", "DATA \"x\": READ i", ); check_stmt_err( - "1:13: In call to READ: 1:18: Cannot assign value of type BOOLEAN to variable of type INTEGER", - "DATA FALSE: READ s%"); + "1:18: Cannot assign value of type BOOLEAN to variable of type INTEGER", + "DATA FALSE: READ s%", + ); } #[test] diff --git a/std/src/exec.rs b/std/src/exec.rs index 94304302..03228b2e 100644 --- a/std/src/exec.rs +++ b/std/src/exec.rs @@ -18,10 +18,8 @@ use async_trait::async_trait; use endbasic_core::ast::ExprType; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; -use endbasic_core::exec::{Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +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; @@ -64,7 +62,7 @@ impl Callable for ClearCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); machine.clear(); Ok(()) @@ -101,7 +99,7 @@ impl Callable for ErrmsgFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); match machine.last_error() { @@ -112,10 +110,10 @@ impl Callable for ErrmsgFunction { } /// Type of the sleep function used by the `SLEEP` command to actually suspend execution. -pub type SleepFn = Box BoxedLocal>; +pub type SleepFn = Box BoxedLocal>>; /// An implementation of a `SleepFn` that stops the current thread. -fn system_sleep(d: Duration, _pos: LineCol) -> BoxedLocal { +fn system_sleep(d: Duration, _pos: LineCol) -> BoxedLocal> { async move { thread::sleep(d); Ok(()) @@ -162,12 +160,12 @@ impl Callable for SleepCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (n, pos) = scope.pop_double_with_pos(); if n < 0.0 { - return Err(CallError::SyntaxError(pos, "Sleep time must be positive".to_owned())); + return Err(Error::SyntaxError(pos, "Sleep time must be positive".to_owned())); } (self.sleep_fn)(Duration::from_secs_f64(n), pos).await @@ -218,7 +216,7 @@ mod tests { fn test_errmsg_after_error() { Tester::default() .run("ON ERROR RESUME NEXT: COLOR -1: PRINT \"Captured: \"; ERRMSG") - .expect_prints(["Captured: 1:23: In call to COLOR: 1:29: Color out of range"]) + .expect_prints(["Captured: 1:29: Color out of range"]) .check(); } @@ -230,31 +228,31 @@ mod tests { #[test] fn test_sleep_ok_int() { - let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal { - async move { Err(CallError::InternalError(pos, format!("Got {} ms", d.as_millis()))) } + let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal> { + async move { Err(Error::InternalError(pos, format!("Got {} ms", d.as_millis()))) } .boxed_local() }; let mut t = Tester::empty().add_callable(SleepCommand::new(Box::from(sleep_fake))); - t.run("SLEEP 123").expect_err("1:1: In call to SLEEP: 1:7: Got 123000 ms").check(); + t.run("SLEEP 123").expect_err("1:7: Got 123000 ms").check(); } #[test] fn test_sleep_ok_float() { - let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal { + let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal> { async move { let ms = d.as_millis(); if ms > 123095 && ms < 123105 { - Err(CallError::InternalError(pos, "Good".to_owned())) + Err(Error::InternalError(pos, "Good".to_owned())) } else { - Err(CallError::InternalError(pos, format!("Bad {}", ms))) + Err(Error::InternalError(pos, format!("Bad {}", ms))) } } .boxed_local() }; let mut t = Tester::empty().add_callable(SleepCommand::new(Box::from(sleep_fake))); - t.run("SLEEP 123.1").expect_err("1:1: In call to SLEEP: 1:7: Good").check(); + t.run("SLEEP 123.1").expect_err("1:7: Good").check(); } #[test] @@ -270,7 +268,7 @@ mod tests { check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3"); check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3"); check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\""); - check_stmt_err("1:1: In call to SLEEP: 1:7: Sleep time must be positive", "SLEEP -1"); - check_stmt_err("1:1: In call to SLEEP: 1:7: Sleep time must be positive", "SLEEP -0.001"); + check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1"); + check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001"); } } diff --git a/std/src/gfx.rs b/std/src/gfx.rs index 3257a03a..32f68f3e 100644 --- a/std/src/gfx.rs +++ b/std/src/gfx.rs @@ -19,10 +19,8 @@ use crate::console::{Console, PixelsXY}; use async_trait::async_trait; use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; -use endbasic_core::exec::{Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +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; @@ -37,31 +35,24 @@ the commands described in HELP \"CONSOLE\", and the pixel-based system, used by described in this section."; /// Parses an expression that represents a single coordinate. -fn parse_coordinate(i: i32, pos: LineCol) -> Result { +fn parse_coordinate(i: i32, pos: LineCol) -> Result { match i16::try_from(i) { Ok(i) => Ok(i), - Err(_) => Err(CallError::SyntaxError(pos, format!("Coordinate {} out of range", i))), + Err(_) => Err(Error::SyntaxError(pos, format!("Coordinate {} out of range", i))), } } /// Parses a pair of expressions that represent an (x,y) coordinate pair. -fn parse_coordinates( - xvalue: i32, - xpos: LineCol, - yvalue: i32, - ypos: LineCol, -) -> Result { +fn parse_coordinates(xvalue: i32, xpos: LineCol, yvalue: i32, ypos: LineCol) -> Result { Ok(PixelsXY { x: parse_coordinate(xvalue, xpos)?, y: parse_coordinate(yvalue, ypos)? }) } /// Parses an expression that represents a radius. -fn parse_radius(i: i32, pos: LineCol) -> Result { +fn parse_radius(i: i32, pos: LineCol) -> Result { match u16::try_from(i) { Ok(i) => Ok(i), - Err(_) if i < 0 => { - Err(CallError::SyntaxError(pos, format!("Radius {} must be positive", i))) - } - Err(_) => Err(CallError::SyntaxError(pos, format!("Radius {} out of range", i))), + Err(_) if i < 0 => Err(Error::SyntaxError(pos, format!("Radius {} must be positive", i))), + Err(_) => Err(Error::SyntaxError(pos, format!("Radius {} out of range", i))), } } @@ -120,7 +111,7 @@ impl Callable for GfxCircleCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(3, scope.nargs()); let (xvalue, xpos) = scope.pop_integer_with_pos(); let (yvalue, ypos) = scope.pop_integer_with_pos(); @@ -129,7 +120,7 @@ impl Callable for GfxCircleCommand { let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?; let r = parse_radius(rvalue, rpos)?; - self.console.borrow_mut().draw_circle(xy, r)?; + self.console.borrow_mut().draw_circle(xy, r).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -188,7 +179,7 @@ impl Callable for GfxCirclefCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(3, scope.nargs()); let (xvalue, xpos) = scope.pop_integer_with_pos(); let (yvalue, ypos) = scope.pop_integer_with_pos(); @@ -197,7 +188,7 @@ impl Callable for GfxCirclefCommand { let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?; let r = parse_radius(rvalue, rpos)?; - self.console.borrow_mut().draw_circle_filled(xy, r)?; + self.console.borrow_mut().draw_circle_filled(xy, r).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -232,9 +223,9 @@ impl Callable for GfxHeightFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - let size = self.console.borrow().size_pixels()?; + let size = self.console.borrow().size_pixels().map_err(|e| scope.io_error(e))?; scope.return_integer(i32::from(size.height)) } } @@ -300,7 +291,7 @@ impl Callable for GfxLineCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(4, scope.nargs()); let (x1value, x1pos) = scope.pop_integer_with_pos(); let (y1value, y1pos) = scope.pop_integer_with_pos(); @@ -310,7 +301,7 @@ impl Callable for GfxLineCommand { let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?; let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?; - self.console.borrow_mut().draw_line(x1y1, x2y2)?; + self.console.borrow_mut().draw_line(x1y1, x2y2).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -362,14 +353,14 @@ impl Callable for GfxPixelCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let (xvalue, xpos) = scope.pop_integer_with_pos(); let (yvalue, ypos) = scope.pop_integer_with_pos(); let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?; - self.console.borrow_mut().draw_pixel(xy)?; + self.console.borrow_mut().draw_pixel(xy).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -436,7 +427,7 @@ impl Callable for GfxRectCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(4, scope.nargs()); let (x1value, x1pos) = scope.pop_integer_with_pos(); let (y1value, y1pos) = scope.pop_integer_with_pos(); @@ -446,7 +437,7 @@ impl Callable for GfxRectCommand { let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?; let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?; - self.console.borrow_mut().draw_rect(x1y1, x2y2)?; + self.console.borrow_mut().draw_rect(x1y1, x2y2).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -512,7 +503,7 @@ impl Callable for GfxRectfCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(4, scope.nargs()); let (x1value, x1pos) = scope.pop_integer_with_pos(); let (y1value, y1pos) = scope.pop_integer_with_pos(); @@ -522,7 +513,7 @@ impl Callable for GfxRectfCommand { let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?; let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?; - self.console.borrow_mut().draw_rect_filled(x1y1, x2y2)?; + self.console.borrow_mut().draw_rect_filled(x1y1, x2y2).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -580,9 +571,9 @@ impl Callable for GfxSyncCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { if scope.nargs() == 0 { - self.console.borrow_mut().sync_now()?; + self.console.borrow_mut().sync_now().map_err(|e| scope.io_error(e))?; Ok(()) } else { debug_assert_eq!(1, scope.nargs()); @@ -590,11 +581,11 @@ impl Callable for GfxSyncCommand { let mut console = self.console.borrow_mut(); if enabled { - console.show_cursor()?; + console.show_cursor().map_err(|e| scope.io_error(e))?; } else { - console.hide_cursor()?; + console.hide_cursor().map_err(|e| scope.io_error(e))?; } - console.set_sync(enabled)?; + console.set_sync(enabled).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -630,9 +621,9 @@ impl Callable for GfxWidthFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - let size = self.console.borrow().size_pixels()?; + let size = self.console.borrow().size_pixels().map_err(|e| scope.io_error(e))?; scope.return_integer(i32::from(size.width)) } } @@ -668,7 +659,7 @@ mod tests { for args in &["-40000, 1, 1, 1", "1, -40000, 1, 1", "1, 1, -40000, 1", "1, 1, 1, -40000"] { let pos = name.len() + 1 + args.find('-').unwrap() + 1; check_stmt_err( - format!("1:1: In call to {}: 1:{}: Coordinate -40000 out of range", name, pos), + format!("1:{}: Coordinate -40000 out of range", pos), &format!("{} {}", name, args), ); } @@ -676,7 +667,7 @@ mod tests { for args in &["40000, 1, 1, 1", "1, 40000, 1, 1", "1, 1, 40000, 1", "1, 1, 1, 40000"] { let pos = name.len() + 1 + args.find('4').unwrap() + 1; check_stmt_err( - format!("1:1: In call to {}: 1:{}: Coordinate 40000 out of range", name, pos), + format!("1:{}: Coordinate 40000 out of range", pos), &format!("{} {}", name, args), ); } @@ -700,28 +691,24 @@ mod tests { for args in &["-40000, 1, 1", "1, -40000, 1"] { let pos = name.len() + 1 + args.find('-').unwrap() + 1; check_stmt_err( - format!("1:1: In call to {}: 1:{}: Coordinate -40000 out of range", name, pos), + format!("1:{}: Coordinate -40000 out of range", pos), &format!("{} {}", name, args), ); } check_stmt_err( - format!( - "1:1: In call to {}: 1:{}: Radius -40000 must be positive", - name, - name.len() + 8 - ), + format!("1:{}: Radius -40000 must be positive", name.len() + 8), &format!("{} 1, 1, -40000", name), ); for args in &["40000, 1, 1", "1, 40000, 1"] { let pos = name.len() + 1 + args.find('4').unwrap() + 1; check_stmt_err( - format!("1:1: In call to {}: 1:{}: Coordinate 40000 out of range", name, pos), + format!("1:{}: Coordinate 40000 out of range", pos), &format!("{} {}", name, args), ); } check_stmt_err( - format!("1:1: In call to {}: 1:{}: Radius 80000 out of range", name, name.len() + 8), + format!("1:{}: Radius 80000 out of range", name.len() + 8), &format!("{} 1, 1, 80000", name), ); @@ -732,7 +719,7 @@ mod tests { } check_stmt_err( - format!("1:1: In call to {}: 1:{}: Radius -1 must be positive", name, name.len() + 8), + format!("1:{}: Radius -1 must be positive", name.len() + 8), &format!("{} 1, 1, -1", name), ); } @@ -792,10 +779,7 @@ mod tests { t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(1, 768)); t.run("result = GFX_HEIGHT").expect_var("result", 768i32).check(); - check_expr_error( - "1:10: In call to GFX_HEIGHT: Graphical console size not yet set", - "GFX_HEIGHT", - ); + check_expr_error("1:10: Graphical console size not yet set", "GFX_HEIGHT"); check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT()"); check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT(1)"); @@ -851,10 +835,7 @@ mod tests { for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] { check_stmt_err( - format!( - "1:1: In call to GFX_PIXEL: 1:{}: Coordinate -40000 out of range", - cmd.find('-').unwrap() + 1 - ), + format!("1:{}: Coordinate -40000 out of range", cmd.find('-').unwrap() + 1), cmd, ); } @@ -938,10 +919,7 @@ mod tests { t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(12345, 1)); t.run("result = GFX_WIDTH").expect_var("result", 12345i32).check(); - check_expr_error( - "1:10: In call to GFX_WIDTH: Graphical console size not yet set", - "GFX_WIDTH", - ); + check_expr_error("1:10: Graphical console size not yet set", "GFX_WIDTH"); check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH()"); check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH(1)"); diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index fe7b279e..242efb23 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -18,16 +18,13 @@ use async_trait::async_trait; use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; -use endbasic_core::exec::{Clearable, Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbols, -}; +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; use std::rc::Rc; -use std::result::Result; mod fakes; pub(crate) use fakes::{MockPins, NoopPins}; @@ -44,12 +41,12 @@ pub struct Pin(pub u8); impl Pin { /// Creates a new pin number from an EndBASIC integer value. - fn from_i32(i: i32, pos: LineCol) -> Result { + fn from_i32(i: i32, pos: LineCol) -> Result { if i < 0 { - return Err(CallError::SyntaxError(pos, format!("Pin number {} must be positive", i))); + return Err(Error::SyntaxError(pos, format!("Pin number {} must be positive", i))); } if i > u8::MAX as i32 { - return Err(CallError::SyntaxError(pos, format!("Pin number {} is too large", i))); + return Err(Error::SyntaxError(pos, format!("Pin number {} is too large", i))); } Ok(Self(i as u8)) } @@ -73,13 +70,13 @@ pub enum PinMode { impl PinMode { /// Obtains a `PinMode` from a value. - fn parse(s: &str, pos: LineCol) -> Result { + fn parse(s: &str, pos: LineCol) -> Result { match s.to_ascii_uppercase().as_ref() { "IN" => Ok(PinMode::In), "IN-PULL-UP" => Ok(PinMode::InPullUp), "IN-PULL-DOWN" => Ok(PinMode::InPullDown), "OUT" => Ok(PinMode::Out), - s => Err(CallError::SyntaxError(pos, format!("Unknown pin mode {}", s))), + s => Err(Error::SyntaxError(pos, format!("Unknown pin mode {}", s))), } } } @@ -180,7 +177,7 @@ impl Callable for GpioSetupCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let pin = { let (i, pos) = scope.pop_integer_with_pos(); @@ -192,8 +189,8 @@ impl Callable for GpioSetupCommand { }; match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.setup(pin, mode)?, - None => self.pins.borrow_mut().setup(pin, mode)?, + Some(mut pins) => pins.setup(pin, mode).map_err(|e| scope.io_error(e))?, + None => self.pins.borrow_mut().setup(pin, mode).map_err(|e| scope.io_error(e))?, }; Ok(()) } @@ -242,11 +239,11 @@ impl Callable for GpioClearCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { if scope.nargs() == 0 { match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.clear_all()?, - None => self.pins.borrow_mut().clear_all()?, + Some(mut pins) => pins.clear_all().map_err(|e| scope.io_error(e))?, + None => self.pins.borrow_mut().clear_all().map_err(|e| scope.io_error(e))?, }; } else { debug_assert_eq!(1, scope.nargs()); @@ -256,8 +253,8 @@ impl Callable for GpioClearCommand { }; match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.clear(pin)?, - None => self.pins.borrow_mut().clear(pin)?, + Some(mut pins) => pins.clear(pin).map_err(|e| scope.io_error(e))?, + None => self.pins.borrow_mut().clear(pin).map_err(|e| scope.io_error(e))?, }; } @@ -304,7 +301,7 @@ impl Callable for GpioReadFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let pin = { let (i, pos) = scope.pop_integer_with_pos(); @@ -312,8 +309,8 @@ impl Callable for GpioReadFunction { }; let value = match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.read(pin)?, - None => self.pins.borrow_mut().read(pin)?, + Some(mut pins) => pins.read(pin).map_err(|e| scope.io_error(e))?, + None => self.pins.borrow_mut().read(pin).map_err(|e| scope.io_error(e))?, }; scope.return_boolean(value) } @@ -366,7 +363,7 @@ impl Callable for GpioWriteCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let pin = { let (i, pos) = scope.pop_integer_with_pos(); @@ -375,8 +372,8 @@ impl Callable for GpioWriteCommand { let value = scope.pop_boolean(); match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.write(pin, value)?, - None => self.pins.borrow_mut().write(pin, value)?, + Some(mut pins) => pins.write(pin, value).map_err(|e| scope.io_error(e))?, + None => self.pins.borrow_mut().write(pin, value).map_err(|e| scope.io_error(e))?, }; Ok(()) } @@ -461,20 +458,11 @@ mod tests { /// features to validate operation. #[test] fn test_real_backend() { - check_stmt_err( - "1:1: In call to GPIO_SETUP: GPIO backend not compiled in", - "GPIO_SETUP 0, \"IN\"", - ); - check_stmt_err("1:1: In call to GPIO_CLEAR: GPIO backend not compiled in", "GPIO_CLEAR"); - check_stmt_err("1:1: In call to GPIO_CLEAR: GPIO backend not compiled in", "GPIO_CLEAR 0"); - check_expr_error( - "1:10: In call to GPIO_READ: GPIO backend not compiled in", - "GPIO_READ(0)", - ); - check_stmt_err( - "1:1: In call to GPIO_WRITE: GPIO backend not compiled in", - "GPIO_WRITE 0, TRUE", - ); + check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_SETUP 0, \"IN\""); + check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR"); + check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_CLEAR 0"); + check_expr_error("1:10: GPIO backend not compiled in", "GPIO_READ(0)"); + check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_WRITE 0, TRUE"); } #[test] @@ -509,16 +497,9 @@ mod tests { check_stmt_compilation_err("1:15: expected STRING but found INTEGER", r#"GPIO_SETUP 1; 2"#); check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1, 2, 3"#); - check_pin_validation( - "1:12: ", - "1:1: In call to GPIO_SETUP: 1:12: ", - r#"GPIO_SETUP _PIN_, "IN""#, - ); + check_pin_validation("1:12: ", "1:12: ", r#"GPIO_SETUP _PIN_, "IN""#); - check_stmt_err( - r#"1:1: In call to GPIO_SETUP: 1:15: Unknown pin mode IN-OUT"#, - r#"GPIO_SETUP 1, "IN-OUT""#, - ); + check_stmt_err(r#"1:15: Unknown pin mode IN-OUT"#, r#"GPIO_SETUP 1, "IN-OUT""#); } #[test] @@ -537,7 +518,7 @@ mod tests { check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | ", r#"GPIO_CLEAR 1,"#); check_stmt_compilation_err("1:1: GPIO_CLEAR expected <> | ", r#"GPIO_CLEAR 1, 2"#); - check_pin_validation("1:12: ", "1:1: In call to GPIO_CLEAR: 1:12: ", r#"GPIO_CLEAR _PIN_"#); + check_pin_validation("1:12: ", "1:12: ", r#"GPIO_CLEAR _PIN_"#); } #[test] @@ -557,11 +538,7 @@ mod tests { check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ()"#); check_expr_compilation_error("1:10: GPIO_READ expected pin%", r#"GPIO_READ(1, 2)"#); - check_pin_validation( - "1:15: ", - "1:5: In call to GPIO_READ: 1:15: ", - r#"v = GPIO_READ(_PIN_)"#, - ); + check_pin_validation("1:15: ", "1:15: ", r#"v = GPIO_READ(_PIN_)"#); } #[test] @@ -582,11 +559,7 @@ mod tests { r#"GPIO_WRITE 1; TRUE"#, ); - check_pin_validation( - "1:12: ", - "1:1: In call to GPIO_WRITE: 1:12: ", - r#"GPIO_WRITE _PIN_, TRUE"#, - ); + check_pin_validation("1:12: ", "1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#); check_stmt_compilation_err( "1:15: expected BOOLEAN but found INTEGER", diff --git a/std/src/help.rs b/std/src/help.rs index f0864a66..8f9c1519 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -20,10 +20,8 @@ use crate::exec::CATEGORY; use async_trait::async_trait; use endbasic_core::ast::ExprType; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; -use endbasic_core::exec::{Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbols, -}; +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; @@ -366,7 +364,7 @@ impl Topics { /// /// If `name` is not long enough to uniquely identify a topic or if the topic does not exist, /// returns an error. - fn find(&self, name: &str, pos: LineCol) -> Result<&dyn Topic, CallError> { + fn find(&self, name: &str, pos: LineCol) -> Result<&dyn Topic> { let key = name.to_ascii_uppercase(); if let Some(topic) = self.0.get(&key) { @@ -381,7 +379,7 @@ impl Topics { _ => { let completions: Vec = children.iter().map(|(name, _topic)| (*name).to_owned()).collect(); - Err(CallError::SyntaxError( + Err(Error::SyntaxError( pos, format!( "Ambiguous help topic {}; candidates are: {}", @@ -392,7 +390,7 @@ impl Topics { } } } - None => Err(CallError::SyntaxError(pos, format!("Unknown help topic {}", name))), + None => Err(Error::SyntaxError(pos, format!("Unknown help topic {}", name))), } } @@ -488,31 +486,31 @@ impl Callable for HelpCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { let topics = Topics::new(machine.get_symbols()); if scope.nargs() == 0 { let mut console = self.console.borrow_mut(); - let previous = console.set_sync(false)?; + let previous = console.set_sync(false).map_err(|e| scope.io_error(e))?; let result = { - let mut pager = Pager::new(&mut *console)?; + let mut pager = Pager::new(&mut *console).map_err(|e| scope.io_error(e))?; self.summary(&topics, &mut pager).await }; - console.set_sync(previous)?; - result?; + console.set_sync(previous).map_err(|e| scope.io_error(e))?; + result.map_err(|e| scope.io_error(e))?; } else { debug_assert_eq!(1, scope.nargs()); let (t, pos) = scope.pop_string_with_pos(); let topic = topics.find(&t, pos)?; let mut console = self.console.borrow_mut(); - let previous = console.set_sync(false)?; + let previous = console.set_sync(false).map_err(|e| scope.io_error(e))?; let result = { - let mut pager = Pager::new(&mut *console)?; + let mut pager = Pager::new(&mut *console).map_err(|e| scope.io_error(e))?; topic.describe(&mut pager).await }; - console.set_sync(previous)?; - result?; + console.set_sync(previous).map_err(|e| scope.io_error(e))?; + result.map_err(|e| scope.io_error(e))?; } Ok(()) @@ -527,7 +525,7 @@ pub fn add_all(machine: &mut Machine, console: Rc>) { #[cfg(test)] pub(crate) mod testutils { use super::*; - use endbasic_core::syms::{CallResult, Callable, CallableMetadata, CallableMetadataBuilder}; + use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; /// A command that does nothing. pub(crate) struct DoNothingCommand { @@ -574,7 +572,7 @@ Second paragraph of the extended description.", &self.metadata } - async fn exec(&self, _scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, _scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { Ok(()) } } @@ -625,7 +623,7 @@ Second paragraph of the extended description.", &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { scope.return_string("irrelevant".to_owned()) } } @@ -942,7 +940,7 @@ This is the first and only topic with just one line. .add_callable(EmptyFunction::new_with_name("ZABC")) .add_callable(EmptyFunction::new_with_name("ZAABC")) .run(r#"help "za""#) - .expect_err("1:1: In call to HELP: 1:6: Ambiguous help topic za; candidates are: ZAABC$, ZAB, ZABC$") + .expect_err("1:6: Ambiguous help topic za; candidates are: ZAABC$, ZAB, ZABC$") .check(); } @@ -959,44 +957,30 @@ This is the first and only topic with just one line. .check(); t.run(r#"HELP 3"#).expect_compilation_err("1:6: expected STRING but found INTEGER").check(); - t.run(r#"HELP "lang%""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic lang%") - .check(); + t.run(r#"HELP "lang%""#).expect_err("1:6: Unknown help topic lang%").check(); - t.run(r#"HELP "foo$""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic foo$") - .check(); - t.run(r#"HELP "foo""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic foo") - .check(); + t.run(r#"HELP "foo$""#).expect_err("1:6: Unknown help topic foo$").check(); + t.run(r#"HELP "foo""#).expect_err("1:6: Unknown help topic foo").check(); - t.run(r#"HELP "do_nothing$""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic do_nothing$") - .check(); - t.run(r#"HELP "empty?""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic empty?") - .check(); + t.run(r#"HELP "do_nothing$""#).expect_err("1:6: Unknown help topic do_nothing$").check(); + t.run(r#"HELP "empty?""#).expect_err("1:6: Unknown help topic empty?").check(); t.run(r#"topic = "foo$": HELP topic$"#) - .expect_err("1:17: In call to HELP: 1:22: Unknown help topic foo$") + .expect_err("1:22: Unknown help topic foo$") .expect_var("topic", "foo$") .check(); let mut t = tester(); - t.run(r#"HELP "undoc""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic undoc") - .check(); + t.run(r#"HELP "undoc""#).expect_err("1:6: Unknown help topic undoc").check(); t.run(r#"undoc = 3: HELP "undoc""#) - .expect_err("1:12: In call to HELP: 1:17: Unknown help topic undoc") + .expect_err("1:17: Unknown help topic undoc") .expect_var("undoc", 3) .check(); let mut t = tester(); - t.run(r#"HELP "undoc""#) - .expect_err("1:1: In call to HELP: 1:6: Unknown help topic undoc") - .check(); + t.run(r#"HELP "undoc""#).expect_err("1:6: Unknown help topic undoc").check(); t.run(r#"DIM undoc(3): HELP "undoc""#) - .expect_err("1:15: In call to HELP: 1:20: Unknown help topic undoc") + .expect_err("1:20: Unknown help topic undoc") .expect_array("undoc", ExprType::Integer, &[3], vec![]) .check(); } diff --git a/std/src/numerics.rs b/std/src/numerics.rs index 9503fc7e..53a9642b 100644 --- a/std/src/numerics.rs +++ b/std/src/numerics.rs @@ -20,10 +20,8 @@ use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredValueSyntax, SingularArgSyntax, }; -use endbasic_core::exec::{Clearable, Machine, Scope}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbols, -}; +use endbasic_core::exec::{Clearable, Error, Machine, Result, Scope}; +use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder, Symbols}; use endbasic_core::value::double_to_integer; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; @@ -56,7 +54,7 @@ impl Clearable for ClearableAngleMode { /// Gets the single argument to a trigonometric function, which is its angle. Applies units /// conversion based on `angle_mode`. -async fn get_angle(scope: &mut Scope<'_>, angle_mode: &AngleMode) -> Result { +async fn get_angle(scope: &mut Scope<'_>, angle_mode: &AngleMode) -> Result { debug_assert_eq!(1, scope.nargs()); let angle = scope.pop_double(); @@ -138,7 +136,7 @@ impl Callable for AtnFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let n = scope.pop_double(); @@ -187,11 +185,11 @@ impl Callable for CintFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (value, pos) = scope.pop_double_with_pos(); - let i = double_to_integer(value).map_err(|e| CallError::SyntaxError(pos, e.to_string()))?; + let i = double_to_integer(value).map_err(|e| Error::SyntaxError(pos, e.to_string()))?; scope.return_integer(i) } } @@ -236,7 +234,7 @@ impl Callable for CosFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let angle = get_angle(&mut scope, &self.angle_mode.borrow()).await?; scope.return_double(angle.cos()) } @@ -272,7 +270,7 @@ impl Callable for DegCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); *self.angle_mode.borrow_mut() = AngleMode::Degrees; Ok(()) @@ -317,12 +315,12 @@ impl Callable for IntFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (value, pos) = scope.pop_double_with_pos(); - let i = double_to_integer(value.floor()) - .map_err(|e| CallError::SyntaxError(pos, e.to_string()))?; + let i = + double_to_integer(value.floor()).map_err(|e| Error::SyntaxError(pos, e.to_string()))?; scope.return_integer(i) } } @@ -361,7 +359,7 @@ impl Callable for MaxFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let mut max = f64::MIN; while scope.nargs() > 0 { let n = scope.pop_double(); @@ -407,7 +405,7 @@ impl Callable for MinFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let mut min = f64::MAX; while scope.nargs() > 0 { let n = scope.pop_double(); @@ -444,7 +442,7 @@ impl Callable for PiFunction { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); scope.return_double(std::f64::consts::PI) } @@ -480,7 +478,7 @@ impl Callable for RadCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); *self.angle_mode.borrow_mut() = AngleMode::Radians; Ok(()) @@ -529,7 +527,7 @@ impl Callable for RandomizeCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { if scope.nargs() == 0 { *self.prng.borrow_mut() = Prng::new_from_entryopy(); } else { @@ -587,7 +585,7 @@ impl Callable for RndFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { if scope.nargs() == 0 { scope.return_double(self.prng.borrow_mut().next()) } else { @@ -596,9 +594,7 @@ impl Callable for RndFunction { match n.cmp(&0) { Ordering::Equal => scope.return_double(self.prng.borrow_mut().last()), Ordering::Greater => scope.return_double(self.prng.borrow_mut().next()), - Ordering::Less => { - Err(CallError::SyntaxError(npos, "n% cannot be negative".to_owned())) - } + Ordering::Less => Err(Error::SyntaxError(npos, "n% cannot be negative".to_owned())), } } } @@ -644,7 +640,7 @@ impl Callable for SinFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let angle = get_angle(&mut scope, &self.angle_mode.borrow()).await?; scope.return_double(angle.sin()) } @@ -681,12 +677,12 @@ impl Callable for SqrFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (num, numpos) = scope.pop_double_with_pos(); if num < 0.0 { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( numpos, "Cannot take square root of a negative number".to_owned(), )); @@ -735,7 +731,7 @@ impl Callable for TanFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let angle = get_angle(&mut scope, &self.angle_mode.borrow()).await?; scope.return_double(angle.tan()) } @@ -792,7 +788,7 @@ mod tests { check_expr_compilation_error("1:10: CINT expected expr#", "CINT(3.0, 4)"); check_expr_error( - "1:10: In call to CINT: 1:15: Cannot cast -1234567890123456 to integer due to overflow", + "1:15: Cannot cast -1234567890123456 to integer due to overflow", "CINT(-1234567890123456.0)", ); } @@ -845,7 +841,7 @@ mod tests { check_expr_compilation_error("1:10: INT expected expr#", "INT(3.0, 4)"); check_expr_error( - "1:10: In call to INT: 1:14: Cannot cast -1234567890123456 to integer due to overflow", + "1:14: Cannot cast -1234567890123456 to integer due to overflow", "INT(-1234567890123456.0)", ); } @@ -937,7 +933,7 @@ mod tests { check_expr_compilation_error("1:10: RND expected <> | ", "RND(1, 7)"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "RND(FALSE)"); - check_expr_error("1:10: In call to RND: 1:14: n% cannot be negative", "RND(-1)"); + check_expr_error("1:14: n% cannot be negative", "RND(-1)"); check_stmt_compilation_err("1:1: RANDOMIZE expected <> | ", "RANDOMIZE ,"); check_stmt_compilation_err("1:11: BOOLEAN is not a number", "RANDOMIZE TRUE"); @@ -967,14 +963,8 @@ mod tests { check_expr_compilation_error("1:10: SQR expected num#", "SQR()"); check_expr_compilation_error("1:14: BOOLEAN is not a number", "SQR(FALSE)"); check_expr_compilation_error("1:10: SQR expected num#", "SQR(3, 4)"); - check_expr_error( - "1:10: In call to SQR: 1:14: Cannot take square root of a negative number", - "SQR(-3)", - ); - check_expr_error( - "1:10: In call to SQR: 1:14: Cannot take square root of a negative number", - "SQR(-0.1)", - ); + check_expr_error("1:14: Cannot take square root of a negative number", "SQR(-3)"); + check_expr_error("1:14: Cannot take square root of a negative number", "SQR(-0.1)"); } #[test] diff --git a/std/src/program.rs b/std/src/program.rs index d1e22e35..47ab4654 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -21,11 +21,9 @@ 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::exec::{self, Machine, Scope, StopReason}; +use endbasic_core::exec::{Machine, Result, Scope, StopReason}; use endbasic_core::parser::parse; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; use std::cell::RefCell; use std::io; @@ -183,22 +181,19 @@ impl Callable for DisasmCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); // TODO(jmmv): We shouldn't have to parse and compile the stored program here. The machine // should hold a copy at all times. let image = { let program = self.program.borrow_mut(); - let ast = match parse(&mut program.text().as_bytes()) { - Ok(ast) => ast, - Err(e) => return Err(CallError::NestedError(exec::Error::ParseError(e))), - }; + let ast = parse(&mut program.text().as_bytes())?; compile(ast, machine.get_symbols())? }; let mut console = self.console.borrow_mut(); - let mut pager = Pager::new(&mut *console)?; + let mut pager = Pager::new(&mut *console).map_err(|e| scope.io_error(e))?; for (addr, instr) in image.instrs.iter().enumerate() { let (op, args) = instr.repr(); let mut line = format!("{:04x} {}", addr, op); @@ -214,9 +209,9 @@ impl Callable for DisasmCommand { } line += &format!(" # {}", pos); } - pager.print(&line).await?; + pager.print(&line).await.map_err(|e| scope.io_error(e))?; } - pager.print("").await?; + pager.print("").await.map_err(|e| scope.io_error(e))?; Ok(()) } @@ -265,12 +260,12 @@ impl Callable for KillCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let name = scope.pop_string(); - let name = add_extension(name)?; - self.storage.borrow_mut().delete(&name).await?; + let name = add_extension(name).map_err(|e| scope.io_error(e))?; + self.storage.borrow_mut().delete(&name).await.map_err(|e| scope.io_error(e))?; Ok(()) } @@ -304,12 +299,12 @@ impl Callable for EditCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); let mut console = self.console.borrow_mut(); let mut program = self.program.borrow_mut(); - program.edit(&mut *console).await?; + program.edit(&mut *console).await.map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -342,13 +337,13 @@ impl Callable for ListCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); let mut console = self.console.borrow_mut(); - let mut pager = Pager::new(&mut *console)?; + let mut pager = Pager::new(&mut *console).map_err(|e| scope.io_error(e))?; for line in self.program.borrow().text().lines() { - pager.print(line).await?; + pager.print(line).await.map_err(|e| scope.io_error(e))?; } Ok(()) } @@ -405,20 +400,26 @@ impl Callable for LoadCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let pathname = scope.pop_string(); - if continue_if_modified(&*self.program.borrow(), &mut *self.console.borrow_mut()).await? { - let pathname = add_extension(pathname)?; - let content = self.storage.borrow().get(&pathname).await?; - let full_name = self.storage.borrow().make_canonical(&pathname)?; + if continue_if_modified(&*self.program.borrow(), &mut *self.console.borrow_mut()) + .await + .map_err(|e| scope.io_error(e))? + { + let pathname = add_extension(pathname).map_err(|e| scope.io_error(e))?; + let content = + self.storage.borrow().get(&pathname).await.map_err(|e| scope.io_error(e))?; + let full_name = + self.storage.borrow().make_canonical(&pathname).map_err(|e| scope.io_error(e))?; self.program.borrow_mut().load(Some(&full_name), &content); machine.clear(); } else { self.console .borrow_mut() - .print("LOAD aborted; use SAVE to save your current changes.")?; + .print("LOAD aborted; use SAVE to save your current changes.") + .map_err(|e| scope.io_error(e))?; } Ok(()) } @@ -461,16 +462,20 @@ impl Callable for NewCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); - if continue_if_modified(&*self.program.borrow(), &mut *self.console.borrow_mut()).await? { + if continue_if_modified(&*self.program.borrow(), &mut *self.console.borrow_mut()) + .await + .map_err(|e| scope.io_error(e))? + { self.program.borrow_mut().load(None, ""); machine.clear(); } else { self.console .borrow_mut() - .print("NEW aborted; use SAVE to save your current changes.")?; + .print("NEW aborted; use SAVE to save your current changes.") + .map_err(|e| scope.io_error(e))?; } Ok(()) } @@ -510,23 +515,22 @@ impl Callable for RunCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); machine.clear(); let program = self.program.borrow().text(); - let stop_reason = match machine.exec(&mut program.as_bytes()).await { - Ok(stop_reason) => stop_reason, - Err(e) => return Err(CallError::NestedError(e)), - }; + let stop_reason = machine.exec(&mut program.as_bytes()).await?; match stop_reason { - StopReason::Break => self.console.borrow_mut().print(BREAK_MSG)?, + StopReason::Break => { + self.console.borrow_mut().print(BREAK_MSG).map_err(|e| scope.io_error(e))? + } stop_reason => { if stop_reason.as_exit_code() != 0 { - self.console.borrow_mut().print(&format!( - "Program exited with code {}", - stop_reason.as_exit_code() - ))?; + self.console + .borrow_mut() + .print(&format!("Program exited with code {}", stop_reason.as_exit_code())) + .map_err(|e| scope.io_error(e))?; } } } @@ -587,15 +591,12 @@ impl Callable for SaveCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let name = if scope.nargs() == 0 { match self.program.borrow().name() { Some(name) => name.to_owned(), None => { - return Err(CallError::IoError(io::Error::new( - io::ErrorKind::InvalidInput, - "Unnamed program; please provide a filename".to_owned(), - ))); + return Err(scope.internal_error("Unnamed program; please provide a filename")); } } } else { @@ -603,13 +604,17 @@ impl Callable for SaveCommand { scope.pop_string() }; - let name = add_extension(name)?; - let full_name = self.storage.borrow().make_canonical(&name)?; + let name = add_extension(name).map_err(|e| scope.io_error(e))?; + let full_name = + self.storage.borrow().make_canonical(&name).map_err(|e| scope.io_error(e))?; let content = self.program.borrow().text(); - self.storage.borrow_mut().put(&name, &content).await?; + self.storage.borrow_mut().put(&name, &content).await.map_err(|e| scope.io_error(e))?; self.program.borrow_mut().set_name(&full_name); - self.console.borrow_mut().print(&format!("Saved as {}", full_name))?; + self.console + .borrow_mut() + .print(&format!("Saved as {}", full_name)) + .map_err(|e| scope.io_error(e))?; Ok(()) } @@ -667,12 +672,12 @@ mod tests { .expect_compilation_err("1:1: KILL expected filename$") .check(); - check_stmt_err("1:1: In call to KILL: Entry not found", r#"KILL "missing-file""#); + check_stmt_err("1:1: Entry not found", r#"KILL "missing-file""#); Tester::default() .write_file("mismatched-extension.bat", "") .run(r#"KILL "mismatched-extension""#) - .expect_err("1:1: In call to KILL: Entry not found") + .expect_err("1:1: Entry not found") .expect_file("MEMORY:/mismatched-extension.bat", "") .check(); } @@ -889,13 +894,13 @@ mod tests { Tester::default() .run(format!(r#"{} "a/b.bas""#, cmd)) - .expect_err(format!("1:1: In call to {}: Too many / separators in path 'a/b.bas'", cmd)) + .expect_err("1:1: Too many / separators in path 'a/b.bas'") .check(); for p in &["foo.bak", "foo.ba", "foo.basic"] { Tester::default() .run(format!(r#"{} "{}""#, cmd, p)) - .expect_err(format!("1:1: In call to {}: Invalid filename extension", cmd)) + .expect_err("1:1: Invalid filename extension") .check(); } } @@ -909,12 +914,12 @@ mod tests { .expect_compilation_err("1:1: LOAD expected filename$") .check(); - check_stmt_err("1:1: In call to LOAD: Entry not found", r#"LOAD "missing-file""#); + check_stmt_err("1:1: Entry not found", r#"LOAD "missing-file""#); Tester::default() .write_file("mismatched-extension.bat", "") .run(r#"LOAD "mismatched-extension""#) - .expect_err("1:1: In call to LOAD: Entry not found") + .expect_err("1:1: Entry not found") .expect_file("MEMORY:/mismatched-extension.bat", "") .check(); } @@ -1077,7 +1082,7 @@ mod tests { .add_input_chars("modified file\n") .run("EDIT: SAVE") .expect_program(None as Option<&str>, "modified file\n") - .expect_err("1:7: In call to SAVE: Unnamed program; please provide a filename") + .expect_err("1:7: Unnamed program; please provide a filename") .check(); } diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index 079ca1e8..ce55ee0c 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -21,8 +21,8 @@ use crate::storage::Storage; use async_trait::async_trait; use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; -use endbasic_core::exec::{Machine, Scope}; -use endbasic_core::syms::{CallResult, Callable, CallableMetadata, CallableMetadataBuilder}; +use endbasic_core::exec::{Machine, Result, Scope}; +use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; use std::cell::RefCell; use std::cmp; @@ -155,11 +155,11 @@ impl Callable for CdCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let target = scope.pop_string(); - self.storage.borrow_mut().cd(&target)?; + self.storage.borrow_mut().cd(&target).map_err(|e| scope.io_error(e))?; Ok(()) } @@ -205,7 +205,7 @@ impl Callable for DirCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { let path = if scope.nargs() == 0 { "".to_owned() } else { @@ -213,7 +213,9 @@ impl Callable for DirCommand { scope.pop_string() }; - show_dir(&self.storage.borrow(), &mut *self.console.borrow_mut(), &path).await?; + show_dir(&self.storage.borrow(), &mut *self.console.borrow_mut(), &path) + .await + .map_err(|e| scope.io_error(e))?; Ok(()) } @@ -273,16 +275,17 @@ impl Callable for MountCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { if scope.nargs() == 0 { - show_drives(&self.storage.borrow_mut(), &mut *self.console.borrow_mut())?; + show_drives(&self.storage.borrow_mut(), &mut *self.console.borrow_mut()) + .map_err(|e| scope.io_error(e))?; Ok(()) } else { debug_assert_eq!(2, scope.nargs()); let target = scope.pop_string(); let name = scope.pop_string(); - self.storage.borrow_mut().mount(&name, &target)?; + self.storage.borrow_mut().mount(&name, &target).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -320,7 +323,7 @@ impl Callable for PwdCommand { &self.metadata } - async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(0, scope.nargs()); let storage = self.storage.borrow(); @@ -328,13 +331,17 @@ impl Callable for PwdCommand { let system_cwd = storage.system_path(&cwd).expect("cwd must return a valid path"); let console = &mut *self.console.borrow_mut(); - console.print("")?; - console.print(&format!(" Working directory: {}", cwd))?; + console.print("").map_err(|e| scope.io_error(e))?; + console.print(&format!(" Working directory: {}", cwd)).map_err(|e| scope.io_error(e))?; match system_cwd { - Some(path) => console.print(&format!(" System location: {}", path.display()))?, - None => console.print(" No system location available")?, + Some(path) => console + .print(&format!(" System location: {}", path.display())) + .map_err(|e| scope.io_error(e))?, + None => { + console.print(" No system location available").map_err(|e| scope.io_error(e))? + } } - console.print("")?; + console.print("").map_err(|e| scope.io_error(e))?; Ok(()) } @@ -378,11 +385,11 @@ impl Callable for UnmountCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let drive = scope.pop_string(); - self.storage.borrow_mut().unmount(&drive)?; + self.storage.borrow_mut().unmount(&drive).map_err(|e| scope.io_error(e))?; Ok(()) } @@ -422,7 +429,7 @@ mod tests { #[test] fn test_cd_errors() { - check_stmt_err("1:1: In call to CD: Drive 'A' is not mounted", "CD \"A:\""); + check_stmt_err("1:1: Drive 'A' is not mounted", "CD \"A:\""); check_stmt_compilation_err("1:1: CD expected path$", "CD"); check_stmt_compilation_err("1:1: CD expected path$", "CD 2, 3"); check_stmt_compilation_err("1:4: expected STRING but found INTEGER", "CD 2"); @@ -693,18 +700,12 @@ mod tests { check_stmt_compilation_err("1:14: expected STRING but found INTEGER", r#"MOUNT "a" AS 1"#); check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"MOUNT 1 AS "a""#); + check_stmt_err("1:1: Invalid drive name 'a:'", r#"MOUNT "memory://" AS "a:""#); check_stmt_err( - "1:1: In call to MOUNT: Invalid drive name 'a:'", - r#"MOUNT "memory://" AS "a:""#, - ); - check_stmt_err( - "1:1: In call to MOUNT: Mount URI must be of the form scheme://path", + "1:1: Mount URI must be of the form scheme://path", r#"MOUNT "foo//bar" AS "a""#, ); - check_stmt_err( - "1:1: In call to MOUNT: Unknown mount scheme 'foo'", - r#"MOUNT "foo://bar" AS "a""#, - ); + check_stmt_err("1:1: Unknown mount scheme 'foo'", r#"MOUNT "foo://bar" AS "a""#); } #[test] @@ -764,7 +765,7 @@ mod tests { check_stmt_compilation_err("1:9: expected STRING but found INTEGER", "UNMOUNT 1"); - check_stmt_err("1:1: In call to UNMOUNT: Invalid drive name 'a:'", "UNMOUNT \"a:\""); - check_stmt_err("1:1: In call to UNMOUNT: Drive 'a' is not mounted", "UNMOUNT \"a\""); + check_stmt_err("1:1: Invalid drive name 'a:'", "UNMOUNT \"a:\""); + check_stmt_err("1:1: Drive 'a' is not mounted", "UNMOUNT \"a\""); } } diff --git a/std/src/strings.rs b/std/src/strings.rs index 70c87a65..573794d3 100644 --- a/std/src/strings.rs +++ b/std/src/strings.rs @@ -20,10 +20,8 @@ use endbasic_core::ast::{ArgSep, ExprType}; use endbasic_core::compiler::{ AnyValueSyntax, ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax, }; -use endbasic_core::exec::{Machine, Scope, ValueTag}; -use endbasic_core::syms::{ - CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, -}; +use endbasic_core::exec::{Error, Machine, Result, Scope, ValueTag}; +use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; use std::cmp::min; use std::convert::TryFrom; @@ -42,7 +40,7 @@ pub fn format_boolean(b: bool) -> &'static str { } /// Parses a string `s` as a boolean. -pub fn parse_boolean(s: &str) -> Result { +pub fn parse_boolean(s: &str) -> std::result::Result { let raw = s.to_uppercase(); if raw == "TRUE" || raw == "YES" || raw == "Y" { Ok(true) @@ -63,7 +61,7 @@ pub fn format_double(d: f64) -> String { } /// Parses a string `s` as a double. -pub fn parse_double(s: &str) -> Result { +pub fn parse_double(s: &str) -> std::result::Result { match s.parse::() { Ok(d) => Ok(d), Err(_) => Err(format!("Invalid double-precision floating point literal {}", s)), @@ -80,7 +78,7 @@ pub fn format_integer(i: i32) -> String { } /// Parses a string `s` as an integer. -pub fn parse_integer(s: &str) -> Result { +pub fn parse_integer(s: &str) -> std::result::Result { match s.parse::() { Ok(d) => Ok(d), Err(_) => Err(format!("Invalid integer literal {}", s)), @@ -124,7 +122,7 @@ impl Callable for AscFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (s, spos) = scope.pop_string_with_pos(); @@ -132,14 +130,14 @@ impl Callable for AscFunction { let ch = match chars.next() { Some(ch) => ch, None => { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( spos, format!("Input string \"{}\" must be 1-character long", s), )); } }; if chars.next().is_some() { - return Err(CallError::SyntaxError( + return Err(Error::SyntaxError( spos, format!("Input string \"{}\" must be 1-character long", s), )); @@ -191,21 +189,18 @@ impl Callable for ChrFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (i, ipos) = scope.pop_integer_with_pos(); if i < 0 { - return Err(CallError::SyntaxError( - ipos, - format!("Character code {} must be positive", i), - )); + return Err(Error::SyntaxError(ipos, format!("Character code {} must be positive", i))); } let code = i as u32; match char::from_u32(code) { Some(ch) => scope.return_string(format!("{}", ch)), - None => Err(CallError::SyntaxError(ipos, format!("Invalid character code {}", code))), + None => Err(Error::SyntaxError(ipos, format!("Invalid character code {}", code))), } } } @@ -257,13 +252,13 @@ impl Callable for LeftFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let s = scope.pop_string(); let (n, npos) = scope.pop_integer_with_pos(); if n < 0 { - Err(CallError::SyntaxError(npos, "n% cannot be negative".to_owned())) + Err(Error::SyntaxError(npos, "n% cannot be negative".to_owned())) } else { let n = min(s.len(), n as usize); scope.return_string(s[..n].to_owned()) @@ -302,12 +297,12 @@ impl Callable for LenFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let (s, spos) = scope.pop_string_with_pos(); if s.len() > i32::MAX as usize { - Err(CallError::InternalError(spos, "String too long".to_owned())) + Err(Error::InternalError(spos, "String too long".to_owned())) } else { scope.return_integer(s.len() as i32) } @@ -345,7 +340,7 @@ impl Callable for LtrimFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let s = scope.pop_string(); @@ -429,7 +424,7 @@ impl Callable for MidFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert!((2..=3).contains(&scope.nargs())); let s = scope.pop_string(); let (start, startpos) = scope.pop_integer_with_pos(); @@ -437,16 +432,13 @@ impl Callable for MidFunction { debug_assert_eq!(0, scope.nargs()); if start < 0 { - return Err(CallError::SyntaxError(startpos, "start% cannot be negative".to_owned())); + return Err(Error::SyntaxError(startpos, "start% cannot be negative".to_owned())); } let start = min(s.len(), start as usize); let end = if let Some((length, lengthpos)) = lengtharg { if length < 0 { - return Err(CallError::SyntaxError( - lengthpos, - "length% cannot be negative".to_owned(), - )); + return Err(Error::SyntaxError(lengthpos, "length% cannot be negative".to_owned())); } min(start + (length as usize), s.len()) } else { @@ -504,13 +496,13 @@ impl Callable for RightFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let s = scope.pop_string(); let (n, npos) = scope.pop_integer_with_pos(); if n < 0 { - Err(CallError::SyntaxError(npos, "n% cannot be negative".to_owned())) + Err(Error::SyntaxError(npos, "n% cannot be negative".to_owned())) } else { let n = min(s.len(), n as usize); scope.return_string(s[s.len() - n..].to_owned()) @@ -549,7 +541,7 @@ impl Callable for RtrimFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let s = scope.pop_string(); @@ -597,7 +589,7 @@ impl Callable for StrFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); match scope.pop_value_tag() { ValueTag::Boolean => { @@ -713,14 +705,8 @@ mod tests { check_expr_compilation_error("1:10: ASC expected char$", r#"ASC()"#); check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"ASC(3)"#); check_expr_compilation_error("1:10: ASC expected char$", r#"ASC("a", 1)"#); - check_expr_error( - "1:10: In call to ASC: 1:14: Input string \"\" must be 1-character long", - r#"ASC("")"#, - ); - check_expr_error( - "1:10: In call to ASC: 1:14: Input string \"ab\" must be 1-character long", - r#"ASC("ab")"#, - ); + check_expr_error("1:14: Input string \"\" must be 1-character long", r#"ASC("")"#); + check_expr_error("1:14: Input string \"ab\" must be 1-character long", r#"ASC("ab")"#); } #[test] @@ -735,14 +721,8 @@ mod tests { check_expr_compilation_error("1:10: CHR expected code%", r#"CHR()"#); check_expr_compilation_error("1:14: BOOLEAN is not a number", r#"CHR(FALSE)"#); check_expr_compilation_error("1:10: CHR expected code%", r#"CHR("a", 1)"#); - check_expr_error( - "1:10: In call to CHR: 1:14: Character code -1 must be positive", - r#"CHR(-1)"#, - ); - check_expr_error( - "1:10: In call to CHR: 1:14: Invalid character code 55296", - r#"CHR(55296)"#, - ); + check_expr_error("1:14: Character code -1 must be positive", r#"CHR(-1)"#); + check_expr_error("1:14: Invalid character code 55296", r#"CHR(55296)"#); } #[test] @@ -765,10 +745,7 @@ mod tests { check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT("", 1, 2)"#); check_expr_compilation_error("1:15: expected STRING but found INTEGER", r#"LEFT(1, 2)"#); check_expr_compilation_error("1:19: STRING is not a number", r#"LEFT("", "")"#); - check_expr_error( - "1:10: In call to LEFT: 1:25: n% cannot be negative", - r#"LEFT("abcdef", -5)"#, - ); + check_expr_error("1:25: n% cannot be negative", r#"LEFT("abcdef", -5)"#); } #[test] @@ -831,14 +808,8 @@ mod tests { ); check_expr_compilation_error("1:19: STRING is not a number", r#"MID(" ", "1", 2)"#); check_expr_compilation_error("1:22: STRING is not a number", r#"MID(" ", 1, "2")"#); - check_expr_error( - "1:10: In call to MID: 1:24: start% cannot be negative", - r#"MID("abcdef", -5, 10)"#, - ); - check_expr_error( - "1:10: In call to MID: 1:27: length% cannot be negative", - r#"MID("abcdef", 3, -5)"#, - ); + check_expr_error("1:24: start% cannot be negative", r#"MID("abcdef", -5, 10)"#); + check_expr_error("1:27: length% cannot be negative", r#"MID("abcdef", 3, -5)"#); } #[test] @@ -855,10 +826,7 @@ mod tests { check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT("", 1, 2)"#); check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RIGHT(1, 2)"#); check_expr_compilation_error("1:20: STRING is not a number", r#"RIGHT("", "")"#); - check_expr_error( - "1:10: In call to RIGHT: 1:26: n% cannot be negative", - r#"RIGHT("abcdef", -5)"#, - ); + check_expr_error("1:26: n% cannot be negative", r#"RIGHT("abcdef", -5)"#); } #[test] diff --git a/web/src/lib.rs b/web/src/lib.rs index e2a907a5..1b9809ab 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -23,8 +23,7 @@ #![warn(unsafe_code)] use async_channel::{Receiver, Sender}; -use endbasic_core::exec::{Signal, YieldNowFn}; -use endbasic_core::syms::{self, CallResult}; +use endbasic_core::exec::{Error, Result, Signal, YieldNowFn}; use endbasic_core::LineCol; use endbasic_std::console::{Console, GraphicsConsole}; use std::cell::RefCell; @@ -105,14 +104,14 @@ fn js_sleep( d: Duration, pos: LineCol, yielder: Rc>, -) -> Pin>> { +) -> Pin>>> { let ms = d.as_millis(); if ms > i32::MAX as u128 { // The JavaScript setTimeout function only takes i32s so ensure our value fits. If it // doesn't, you can imagine chaining calls to setTimeout to achieve the desired delay... // but the numbers we are talking about are so big that this doesn't make sense. return Box::pin(async move { - Err(syms::CallError::InternalError(pos, "Cannot sleep for that long".to_owned())) + Err(Error::InternalError(pos, "Cannot sleep for that long".to_owned())) }); } let ms = ms as i32; @@ -363,7 +362,7 @@ pub fn get_build_id() -> String { } /// Module initialization. -pub fn main() -> Result<(), JsValue> { +pub fn main() -> std::result::Result<(), JsValue> { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); @@ -396,7 +395,7 @@ mod tests { .await .unwrap_err() { - syms::CallError::InternalError(pos, e) => { + Error::InternalError(pos, e) => { assert_eq!(LineCol { line: 1, col: 2 }, pos); assert_eq!("Cannot sleep for that long", e); } From 0442270e28669fc359d1d4897c4b2523906ca222 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 10 Nov 2024 09:41:56 -0800 Subject: [PATCH 010/110] Stream parsing through the compiler Instead of parsing all statements, buffering them into memory and then feeding them in one go to the compiler, interweave the two steps. This allows the compilation process to "pull" statements from the parser, which is more efficient and addresses an old TODO that I added when I first implemented the compilation step. --- core/src/compiler/mod.rs | 36 +++++++++++++++++++++++++++--------- core/src/exec.rs | 11 +---------- core/src/parser.rs | 35 +++++++++++++++++++++++------------ std/src/program.rs | 4 +--- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 22053002..765d7958 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -17,12 +17,14 @@ use crate::ast::*; use crate::bytecode::*; +use crate::parser; use crate::reader::LineCol; use crate::syms::{CallableMetadata, CallableMetadataBuilder, Symbol, SymbolKey, Symbols}; use std::borrow::Cow; use std::collections::HashMap; #[cfg(test)] use std::collections::HashSet; +use std::io; mod args; pub use args::*; @@ -54,6 +56,9 @@ pub enum Error { #[error("{0}: Cannot index non-array {1}")] IndexNonArray(LineCol, String), + #[error("{0}: I/O error during compilation: {1}")] + IoError(LineCol, io::Error), + #[error("{0}: EXIT DO outside of DO loop")] MisplacedExitDo(LineCol), @@ -75,6 +80,9 @@ pub enum Error { #[error("{0}: {1} is not an array nor a function")] NotArrayOrFunction(LineCol, SymbolKey), + #[error("{0}: {1}")] + ParseError(LineCol, String), + #[error("{0}: Cannot define already-defined symbol {1}")] RedefinitionError(LineCol, SymbolKey), @@ -91,6 +99,15 @@ pub enum Error { UnknownLabel(LineCol, String), } +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), + } + } +} + /// Result for compiler return values. pub type Result = std::result::Result; @@ -1086,9 +1103,11 @@ impl Compiler { /// /// `symtable` is the symbols table as used by the compiler and should be prepopulated with any /// callables that the compiled program should recognize. -fn compile_aux(stmts: Vec, symtable: SymbolsTable) -> Result<(Image, SymbolsTable)> { +fn compile_aux(input: &mut dyn io::Read, symtable: SymbolsTable) -> Result<(Image, SymbolsTable)> { let mut compiler = Compiler { symtable, ..Default::default() }; - compiler.compile_many(stmts)?; + for stmt in parser::parse(input) { + compiler.compile_one(stmt?)?; + } compiler.to_image() } @@ -1098,21 +1117,20 @@ fn compile_aux(stmts: Vec, symtable: SymbolsTable) -> Result<(Image, /// that exist in the virtual machine. // TODO(jmmv): This is ugly. Now that we have a symbols table in here, we should not _also_ have a // Symbols object to maintain runtime state (or if we do, we shouldn't be getting it here). -pub fn compile(stmts: Vec, syms: &Symbols) -> Result { - compile_aux(stmts, SymbolsTable::from(syms)).map(|(image, _symtable)| image) +pub fn compile(input: &mut dyn io::Read, syms: &Symbols) -> Result { + compile_aux(input, SymbolsTable::from(syms)).map(|(image, _symtable)| image) } #[cfg(test)] mod testutils { use super::*; - use crate::parser; use crate::syms::CallableMetadataBuilder; /// Builder pattern to prepare the compiler for testing purposes. #[derive(Default)] #[must_use] pub(crate) struct Tester { - stmts: Vec, + source: String, symtable: SymbolsTable, } @@ -1139,8 +1157,8 @@ mod testutils { /// Parses and appends statements to the list of statements to be compiled. pub(crate) fn parse(mut self, input: &str) -> Self { - let mut stmts = parser::parse(&mut input.as_bytes()).unwrap(); - self.stmts.append(&mut stmts); + self.source.push_str(input); + self.source.push('\n'); self } @@ -1148,7 +1166,7 @@ mod testutils { /// validate expectations about the compilation. pub(crate) fn compile(self) -> Checker { Checker { - result: compile_aux(self.stmts, self.symtable), + result: compile_aux(&mut self.source.as_bytes(), self.symtable), exp_error: None, ignore_instrs: false, exp_instrs: vec![], diff --git a/core/src/exec.rs b/core/src/exec.rs index ca3564cd..7c6cad40 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -18,7 +18,6 @@ use crate::ast::*; use crate::bytecode::*; use crate::compiler; -use crate::parser; use crate::reader::LineCol; use crate::syms::{Callable, Symbol, SymbolKey, Symbols}; use crate::value; @@ -48,10 +47,6 @@ pub enum Error { #[error("{0}: {1}")] IoError(LineCol, io::Error), - /// Parsing error during execution. - #[error("{0}")] - ParseError(#[from] parser::Error), - /// Syntax error. #[error("{0}: {1}")] SyntaxError(LineCol, String), @@ -70,7 +65,6 @@ impl Error { Error::EvalError(..) => true, Error::InternalError(..) => true, Error::IoError(..) => true, - Error::ParseError(_) => false, Error::SyntaxError(..) => true, } } @@ -1473,10 +1467,7 @@ impl Machine { debug_assert!(!self.check_stop); debug_assert!(self.stop_reason.is_none()); - // TODO(jmmv): It should be possible to make the parser return statements one at a time and - // stream them to the compiler, instead of buffering everything in a vector. - let stmts = parser::parse(input)?; - let image = compiler::compile(stmts, &self.symbols)?; + let image = compiler::compile(input, &self.symbols)?; assert!(self.data.is_empty()); self.data = image.data; diff --git a/core/src/parser.rs b/core/src/parser.rs index 2c87af20..54631be5 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -1955,14 +1955,21 @@ impl<'a> Parser<'a> { } } -/// Extracts all statements from the input stream. -pub fn parse(input: &mut dyn io::Read) -> Result> { - let mut parser = Parser::from(input); - let mut statements = vec![]; - while let Some(statement) = parser.parse_one_safe()? { - statements.push(statement); +pub(crate) struct StatementIter<'a> { + parser: Parser<'a>, +} + +impl<'a> Iterator for StatementIter<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + self.parser.parse_one_safe().transpose() } - Ok(statements) +} + +/// Extracts all statements from the input stream. +pub(crate) fn parse(input: &mut dyn io::Read) -> StatementIter { + StatementIter { parser: Parser::from(input) } } #[cfg(test)] @@ -2025,7 +2032,8 @@ mod tests { /// `exp_statements`. fn do_ok_test(input: &str, exp_statements: &[Statement]) { let mut input = input.as_bytes(); - let statements = parse(&mut input).expect("Parsing failed"); + let statements = + parse(&mut input).map(|r| r.expect("Parsing failed")).collect::>(); assert_eq!(exp_statements, statements.as_slice()); } @@ -2047,10 +2055,13 @@ mod tests { // parsed next. fn do_error_test_no_reset(input: &str, expected_err: &str) { let mut input = input.as_bytes(); - assert_eq!( - expected_err, - format!("{}", parse(&mut input).expect_err("Parsing did not fail")) - ); + for result in parse(&mut input) { + if let Err(e) = result { + assert_eq!(expected_err, format!("{}", e)); + return; + } + } + panic!("Parsing did not fail") } #[test] diff --git a/std/src/program.rs b/std/src/program.rs index 47ab4654..dca447b4 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -22,7 +22,6 @@ use async_trait::async_trait; use endbasic_core::ast::ExprType; use endbasic_core::compiler::{compile, ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax}; use endbasic_core::exec::{Machine, Result, Scope, StopReason}; -use endbasic_core::parser::parse; use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; use std::cell::RefCell; @@ -188,8 +187,7 @@ impl Callable for DisasmCommand { // should hold a copy at all times. let image = { let program = self.program.borrow_mut(); - let ast = parse(&mut program.text().as_bytes())?; - compile(ast, machine.get_symbols())? + compile(&mut program.text().as_bytes(), machine.get_symbols())? }; let mut console = self.console.borrow_mut(); From d1213b9efce2ae77bfb87e5245aa5682e1b6a0e3 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 10 Nov 2024 09:49:51 -0800 Subject: [PATCH 011/110] Update npm dependencies --- .github/workflows/publish.sh | 2 +- web/package-lock.json | 563 ++++++++++++++++++----------------- web/package.json | 8 +- 3 files changed, 291 insertions(+), 282 deletions(-) diff --git a/.github/workflows/publish.sh b/.github/workflows/publish.sh index 49671cc0..27bf0472 100755 --- a/.github/workflows/publish.sh +++ b/.github/workflows/publish.sh @@ -19,7 +19,7 @@ set -eux node_env="${1}"; shift curl -o- \ - https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash + https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash export NVM_DIR="${HOME}/.nvm" [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh" diff --git a/web/package-lock.json b/web/package-lock.json index c0298f28..a842d60a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,13 +12,13 @@ "jquery": "3.7.1" }, "devDependencies": { - "@wasm-tool/wasm-pack-plugin": "^1.6.0", + "@wasm-tool/wasm-pack-plugin": "^1.7.0", "copy-webpack-plugin": "^12.0.2", - "html-webpack-plugin": "^5.6.0", + "html-webpack-plugin": "^5.6.3", "rimraf": "^6.0.1", - "webpack": "^5.90.3", + "webpack": "^5.96.1", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.2" + "webpack-dev-server": "^5.1.0" } }, "node_modules/@discoveryjs/json-ext": { @@ -171,9 +171,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.3.0.tgz", - "integrity": "sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", "dev": true, "engines": { "node": ">=10.0" @@ -227,16 +227,6 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", @@ -287,10 +277,30 @@ "@types/node": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/express": { @@ -306,9 +316,21 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.5", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", - "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "dev": true, "dependencies": { "@types/node": "*", @@ -330,9 +352,9 @@ "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.14", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", - "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -351,12 +373,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.8" } }, "node_modules/@types/node-forge": { @@ -369,9 +391,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", "dev": true }, "node_modules/@types/range-parser": { @@ -426,9 +448,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.11", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", - "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dev": true, "dependencies": { "@types/node": "*" @@ -447,148 +469,148 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -661,10 +683,19 @@ "node": ">= 0.6" } }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -673,15 +704,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -827,15 +849,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -874,9 +887,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -893,10 +906,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -927,9 +940,9 @@ } }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "engines": { "node": ">= 0.8" @@ -965,9 +978,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001642", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", - "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "version": "1.0.30001679", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", + "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", "dev": true, "funding": [ { @@ -1118,29 +1131,23 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", "dev": true, "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", + "negotiator": "~0.6.4", "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -1172,9 +1179,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -1217,9 +1224,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -1448,9 +1455,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.827", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", - "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==", + "version": "1.5.55", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", + "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", "dev": true }, "node_modules/emoji-regex": { @@ -1491,9 +1498,9 @@ } }, "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -1530,9 +1537,9 @@ "dev": true }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -1621,9 +1628,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -1631,7 +1638,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -1703,9 +1710,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", - "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", "dev": true }, "node_modules/fastest-levenshtein": { @@ -1791,9 +1798,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -1844,6 +1851,20 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2103,9 +2124,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", + "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -2196,9 +2217,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, "dependencies": { "@types/http-proxy": "^1.17.8", @@ -2241,18 +2262,18 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "dependencies": { "pkg-dir": "^4.2.0", @@ -2305,9 +2326,9 @@ } }, "node_modules/is-core-module": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", - "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { "hasown": "^2.0.2" @@ -2464,9 +2485,9 @@ } }, "node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -2476,9 +2497,6 @@ }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jest-worker": { @@ -2546,9 +2564,9 @@ } }, "node_modules/launch-editor": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.0.tgz", - "integrity": "sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -2592,9 +2610,9 @@ } }, "node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", "dev": true, "engines": { "node": "20 || >=22" @@ -2610,9 +2628,9 @@ } }, "node_modules/memfs": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", - "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz", + "integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==", "dev": true, "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", @@ -2757,9 +2775,9 @@ } }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "engines": { "node": ">= 0.6" @@ -2791,9 +2809,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/normalize-path": { @@ -2818,9 +2836,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "dev": true, "engines": { "node": ">= 0.4" @@ -2928,9 +2946,9 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, "node_modules/param-case": { @@ -3021,9 +3039,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -3165,15 +3183,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3718,12 +3727,12 @@ } }, "node_modules/spdy-transport/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3735,18 +3744,18 @@ } }, "node_modules/spdy-transport/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/spdy/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3758,9 +3767,9 @@ } }, "node_modules/spdy/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/statuses": { @@ -3905,9 +3914,9 @@ } }, "node_modules/terser": { - "version": "5.31.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", - "integrity": "sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==", + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -4067,9 +4076,9 @@ } }, "node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, "node_modules/type-is": { @@ -4086,9 +4095,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, "node_modules/unicorn-magic": { @@ -4113,9 +4122,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -4132,8 +4141,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -4191,9 +4200,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -4213,18 +4222,18 @@ } }, "node_modules/webpack": { - "version": "5.94.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", - "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", "dev": true, "dependencies": { - "@types/estree": "^1.0.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", diff --git a/web/package.json b/web/package.json index cedf4f9e..83f9eda1 100644 --- a/web/package.json +++ b/web/package.json @@ -37,12 +37,12 @@ "jquery": "3.7.1" }, "devDependencies": { - "@wasm-tool/wasm-pack-plugin": "^1.6.0", + "@wasm-tool/wasm-pack-plugin": "^1.7.0", "copy-webpack-plugin": "^12.0.2", - "html-webpack-plugin": "^5.6.0", - "webpack": "^5.90.3", + "html-webpack-plugin": "^5.6.3", + "webpack": "^5.96.1", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.0.2", + "webpack-dev-server": "^5.1.0", "rimraf": "^6.0.1" } } From 634d2e1591722bf93223a30c1e7519a214b6484b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 10 Nov 2024 10:09:32 -0800 Subject: [PATCH 012/110] Fix deprecation warnings coming from web_sys --- web/src/canvas.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/web/src/canvas.rs b/web/src/canvas.rs index ac8cf311..6abc2aef 100644 --- a/web/src/canvas.rs +++ b/web/src/canvas.rs @@ -48,15 +48,15 @@ pub(crate) fn js_value_to_io_error(e: JsValue) -> io::Error { /// Returns the 2D rendering context for a given `canvas` element. fn html_canvas_to_2d_context(canvas: HtmlCanvasElement) -> io::Result { - let mut attrs = ContextAttributes2d::new(); + let attrs = ContextAttributes2d::new(); // We don't use transparency for anything, so disable the alpha channel for performance reasons. - attrs.alpha(false); + attrs.set_alpha(false); // Chrome recommends setting this to true because we read from the canvas to move the cursor // and to scroll the console, but these operations needn't be fast. It seems better to keep // this disabled to optimize for the rendering path of graphical applications. - attrs.will_read_frequently(false); + attrs.set_will_read_frequently(false); let context = match canvas .get_context_with_context_options("2d", &attrs) @@ -194,10 +194,7 @@ impl CanvasRasterOps { /// Sets the fill color of the canvas to `rgb`. fn set_fill_style_rgb(&mut self, rgb: RGB) { if self.fill_color != rgb { - self.context.set_fill_style(&JsValue::from_str(&format!( - "rgb({}, {}, {})", - rgb.0, rgb.1, rgb.2 - ))); + self.context.set_fill_style_str(&format!("rgb({}, {}, {})", rgb.0, rgb.1, rgb.2)); self.fill_color = rgb; } } @@ -205,10 +202,7 @@ impl CanvasRasterOps { /// Sets the stroke color of the canvas to `rgb`. fn set_stroke_style_rgb(&mut self, rgb: RGB) { if self.stroke_color != rgb { - self.context.set_stroke_style(&JsValue::from_str(&format!( - "rgb({}, {}, {})", - rgb.0, rgb.1, rgb.2 - ))); + self.context.set_stroke_style_str(&format!("rgb({}, {}, {})", rgb.0, rgb.1, rgb.2)); self.stroke_color = rgb; } } From 231cd3f252e4ca00689093d9dcdffbefcc5d5571 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 10 Nov 2024 10:42:22 -0800 Subject: [PATCH 013/110] Fix execution of SDL2 tests on Windows For some reason, the linker has started to look for libraries in a different location than the setup-sdl.ps1 script expected and it cannot find the SDL2 libraries anymore. I suspect, but have no proof, that the linker is now just running from the "current directory" so place the libraries at the top of the source tree as well. This seems to work but feels like a hack: a better option would probably be to play with build.rs and try to pass the right flags to the linker, but that's not something I want to chase right now if this workaround works. --- .github/workflows/setup-sdl.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/setup-sdl.ps1 b/.github/workflows/setup-sdl.ps1 index 0ab8612f..c0418490 100644 --- a/.github/workflows/setup-sdl.ps1 +++ b/.github/workflows/setup-sdl.ps1 @@ -32,10 +32,12 @@ Copy-Item .\SDL2-2.26.1\lib\x64\*.txt,.\SDL2_ttf-2.0.15\lib\x64\*.txt dlls Remove-Item -Recurse -Force .\SDL2-2.26.1,.\SDL2_ttf-2.0.15,SDL2*.zip foreach ($dir in + ".", "target", "target\debug", "target\debug\deps", "target\debug\examples", "target\release", "target\release\deps", "target\release\examples") { [void](New-Item -Force -Type Directory $dir) + Write-Output "Copying SDL2 libs and dlls to $dir" Copy-Item dlls\* $dir Copy-Item libs\* $dir } From f59dc8e1acd528bd5c7cde9bccbce05f5015a53a Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 11 Nov 2024 07:53:34 -0800 Subject: [PATCH 014/110] Make the core bytecode interpreter sync The bytecode interpreter has to be async in order to process builtin callables and stop signals. However, these async operations are infrequent, and the inner execution loop can be made sync. Just doing this gives us a 25% performance boost. But we can go further, clean up the way stop signals are handled, and simplify the core execution loop. With all these changes, I'm observing an almost 3x performance improvement on a trivial async-less benchmark. --- NEWS.md | 2 + core/src/exec.rs | 936 ++++++++++++++++++++++++++--------------------- 2 files changed, 517 insertions(+), 421 deletions(-) diff --git a/NEWS.md b/NEWS.md index 3c2839ac..e60a52ce 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,8 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Cleaned up internal error handling, which changes how errors returned from commands and functions are displayed to the user. +* Improved the performance of the core interpreter by more than 2x. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/core/src/exec.rs b/core/src/exec.rs index 7c6cad40..efb21022 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -85,11 +85,53 @@ pub enum Signal { Break, } +/// Request to exit the VM execution loop to execute a native command or function. +#[derive(Clone, Debug, Eq, PartialEq)] +struct UpcallData { + /// Name of the callable to execute. + name: SymbolKey, + + /// Expected type of the value returned by the callable (if a function). + return_type: Option, + + /// Position of the invocation. + pos: LineCol, + + /// Number of arguments to extract from the stack for the invocation. + nargs: usize, +} + +/// Describes how the machine stopped execution while it was running a portion of a script. +/// +/// This is different from `StopReason` which represents "user-visible" stop conditions. Instead, +/// the stop conditions represented by this enum are "internal". The bytecode interpreter may have +/// to exit from its inner loop to perform "expensive" operations, which are all still handled here. +/// +/// One reason for the entries in this enum, such as `CheckStop` and `Upcall`, is to keep the tight +/// inner loop of the bytecode interpreter sync. Early benchmarks showed a 25% performance +/// improvement just by removing async from the loop and pushing the infrequent async awaits to an +/// outer loop. +enum InternalStopReason { + /// Execution terminated because the bytecode reached a point in the instructions where an + /// interruption, if any, should be processed. + CheckStop, + + /// Execution terminated because the machine reached the end of the input. + Eof, + + /// Execution terminated because the machine was asked to terminate with `END`. + Exited(u8), + + /// Execution terminated because the bytecode requires the caller to issue a builtin function + /// or command call. + Upcall(UpcallData), +} + /// Describes how the machine stopped execution while it was running a script via `exec()`. #[derive(Clone, Debug, Eq, PartialEq)] #[must_use] pub enum StopReason { - /// Execution terminates because the machine reached the end of the input. + /// Execution terminated because the machine reached the end of the input. Eof, /// Execution terminated because the machine was asked to terminate with `END`. @@ -522,8 +564,6 @@ pub struct Machine { clearables: Vec>, yield_now_fn: Option, signals_chan: (Sender, Receiver), - check_stop: bool, - stop_reason: Option, last_error: Option, data: Vec>, } @@ -551,8 +591,6 @@ impl Machine { clearables: vec![], yield_now_fn, signals_chan: signals, - check_stop: false, - stop_reason: None, last_error: None, data: vec![], } @@ -614,15 +652,10 @@ impl Machine { } match self.signals_chan.1.try_recv() { - Ok(Signal::Break) => { - self.check_stop = true; - self.stop_reason = Some(StopReason::Break) - } - Err(TryRecvError::Empty) => (), + Ok(Signal::Break) => true, + Err(TryRecvError::Empty) => false, Err(TryRecvError::Closed) => panic!("Channel unexpectedly closed"), } - - self.stop_reason.is_some() } /// Handles an array assignment. @@ -699,7 +732,7 @@ impl Machine { } /// Tells the machine to stop execution at the next statement boundary. - fn end(&mut self, context: &mut Context, has_code: bool) -> Result<()> { + fn end(&mut self, context: &mut Context, has_code: bool) -> Result { let code = if has_code { let (code, code_pos) = context.value_stack.pop_integer_with_pos(); if code < 0 { @@ -718,9 +751,7 @@ impl Machine { } else { 0 }; - self.check_stop = true; - self.stop_reason = Some(StopReason::Exited(code)); - Ok(()) + Ok(InternalStopReason::Exited(code)) } /// Handles a unary logical operator that cannot fail. @@ -939,7 +970,11 @@ impl Machine { )); } let f = f.clone(); - self.do_function_call(context, return_type, fref_pos, nargs, f).await + if f.metadata().is_argless() { + self.argless_function_call(context, name, return_type, fref_pos, f).await + } else { + self.do_function_call(context, return_type, fref_pos, nargs, f).await + } } _ => unreachable!("Function existence and type checking has been done at compile time"), } @@ -980,483 +1015,562 @@ impl Machine { } } - /// Helper for `exec_one` that only worries about execution of a single instruction. - /// - /// Errors are handled on the caller side depending on the `ON ERROR` handling policy that is - /// currently configured. - async fn exec_safe(&mut self, context: &mut Context, instrs: &[Instruction]) -> Result<()> { - let instr = &instrs[context.pc]; - match instr { - Instruction::LogicalAnd(pos) => { - Machine::exec_logical_op2(context, |lhs, rhs| lhs && rhs, *pos); - context.pc += 1; - } - - Instruction::LogicalOr(pos) => { - Machine::exec_logical_op2(context, |lhs, rhs| lhs || rhs, *pos); - context.pc += 1; - } + /// Executes as many instructions as possible from `instrs`, starting at `context.pc`, until an + /// instruction asks to stop or execution reaches the end of the program. + fn exec_until_stop( + &mut self, + context: &mut Context, + instrs: &[Instruction], + ) -> Result { + while context.pc < instrs.len() { + let instr = &instrs[context.pc]; + match instr { + Instruction::LogicalAnd(pos) => { + Machine::exec_logical_op2(context, |lhs, rhs| lhs && rhs, *pos); + context.pc += 1; + } - Instruction::LogicalXor(pos) => { - Machine::exec_logical_op2(context, |lhs, rhs| lhs ^ rhs, *pos); - context.pc += 1; - } + Instruction::LogicalOr(pos) => { + Machine::exec_logical_op2(context, |lhs, rhs| lhs || rhs, *pos); + context.pc += 1; + } - Instruction::LogicalNot(pos) => { - Machine::exec_logical_op1(context, |rhs| !rhs, *pos); - context.pc += 1; - } + Instruction::LogicalXor(pos) => { + Machine::exec_logical_op2(context, |lhs, rhs| lhs ^ rhs, *pos); + context.pc += 1; + } - Instruction::BitwiseAnd(pos) => { - Machine::exec_bitwise_op2(context, |lhs, rhs| lhs & rhs, *pos); - context.pc += 1; - } + Instruction::LogicalNot(pos) => { + Machine::exec_logical_op1(context, |rhs| !rhs, *pos); + context.pc += 1; + } - Instruction::BitwiseOr(pos) => { - Machine::exec_bitwise_op2(context, |lhs, rhs| lhs | rhs, *pos); - context.pc += 1; - } + Instruction::BitwiseAnd(pos) => { + Machine::exec_bitwise_op2(context, |lhs, rhs| lhs & rhs, *pos); + context.pc += 1; + } - Instruction::BitwiseXor(pos) => { - Machine::exec_bitwise_op2(context, |lhs, rhs| lhs ^ rhs, *pos); - context.pc += 1; - } + Instruction::BitwiseOr(pos) => { + Machine::exec_bitwise_op2(context, |lhs, rhs| lhs | rhs, *pos); + context.pc += 1; + } - Instruction::BitwiseNot(pos) => { - Machine::exec_bitwise_op1(context, |rhs| !rhs, *pos); - context.pc += 1; - } + Instruction::BitwiseXor(pos) => { + Machine::exec_bitwise_op2(context, |lhs, rhs| lhs ^ rhs, *pos); + context.pc += 1; + } - Instruction::ShiftLeft(pos) => { - Machine::exec_bitwise_op2_err(context, value::bitwise_shl, *pos)?; - context.pc += 1; - } + Instruction::BitwiseNot(pos) => { + Machine::exec_bitwise_op1(context, |rhs| !rhs, *pos); + context.pc += 1; + } - Instruction::ShiftRight(pos) => { - Machine::exec_bitwise_op2_err(context, value::bitwise_shr, *pos)?; - context.pc += 1; - } + Instruction::ShiftLeft(pos) => { + Machine::exec_bitwise_op2_err(context, value::bitwise_shl, *pos)?; + context.pc += 1; + } - Instruction::EqualBooleans(pos) => { - Machine::exec_equality_boolean_op2(context, |lhs, rhs| lhs == rhs, *pos); - context.pc += 1; - } + Instruction::ShiftRight(pos) => { + Machine::exec_bitwise_op2_err(context, value::bitwise_shr, *pos)?; + context.pc += 1; + } - Instruction::NotEqualBooleans(pos) => { - Machine::exec_equality_boolean_op2(context, |lhs, rhs| lhs != rhs, *pos); - context.pc += 1; - } + Instruction::EqualBooleans(pos) => { + Machine::exec_equality_boolean_op2(context, |lhs, rhs| lhs == rhs, *pos); + context.pc += 1; + } - Instruction::EqualDoubles(pos) => { - Machine::exec_equality_double_op2(context, |lhs, rhs| lhs == rhs, *pos); - context.pc += 1; - } + Instruction::NotEqualBooleans(pos) => { + Machine::exec_equality_boolean_op2(context, |lhs, rhs| lhs != rhs, *pos); + context.pc += 1; + } - Instruction::NotEqualDoubles(pos) => { - Machine::exec_equality_double_op2(context, |lhs, rhs| lhs != rhs, *pos); - context.pc += 1; - } + Instruction::EqualDoubles(pos) => { + Machine::exec_equality_double_op2(context, |lhs, rhs| lhs == rhs, *pos); + context.pc += 1; + } - Instruction::LessDoubles(pos) => { - Machine::exec_equality_double_op2(context, |lhs, rhs| lhs < rhs, *pos); - context.pc += 1; - } + Instruction::NotEqualDoubles(pos) => { + Machine::exec_equality_double_op2(context, |lhs, rhs| lhs != rhs, *pos); + context.pc += 1; + } - Instruction::LessEqualDoubles(pos) => { - Machine::exec_equality_double_op2(context, |lhs, rhs| lhs <= rhs, *pos); - context.pc += 1; - } + Instruction::LessDoubles(pos) => { + Machine::exec_equality_double_op2(context, |lhs, rhs| lhs < rhs, *pos); + context.pc += 1; + } - Instruction::GreaterDoubles(pos) => { - Machine::exec_equality_double_op2(context, |lhs, rhs| lhs > rhs, *pos); - context.pc += 1; - } + Instruction::LessEqualDoubles(pos) => { + Machine::exec_equality_double_op2(context, |lhs, rhs| lhs <= rhs, *pos); + context.pc += 1; + } - Instruction::GreaterEqualDoubles(pos) => { - Machine::exec_equality_double_op2(context, |lhs, rhs| lhs >= rhs, *pos); - context.pc += 1; - } + Instruction::GreaterDoubles(pos) => { + Machine::exec_equality_double_op2(context, |lhs, rhs| lhs > rhs, *pos); + context.pc += 1; + } - Instruction::EqualIntegers(pos) => { - Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs == rhs, *pos); - context.pc += 1; - } + Instruction::GreaterEqualDoubles(pos) => { + Machine::exec_equality_double_op2(context, |lhs, rhs| lhs >= rhs, *pos); + context.pc += 1; + } - Instruction::NotEqualIntegers(pos) => { - Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs != rhs, *pos); - context.pc += 1; - } + Instruction::EqualIntegers(pos) => { + Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs == rhs, *pos); + context.pc += 1; + } - Instruction::LessIntegers(pos) => { - Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs < rhs, *pos); - context.pc += 1; - } + Instruction::NotEqualIntegers(pos) => { + Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs != rhs, *pos); + context.pc += 1; + } - Instruction::LessEqualIntegers(pos) => { - Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs <= rhs, *pos); - context.pc += 1; - } + Instruction::LessIntegers(pos) => { + Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs < rhs, *pos); + context.pc += 1; + } - Instruction::GreaterIntegers(pos) => { - Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs > rhs, *pos); - context.pc += 1; - } + Instruction::LessEqualIntegers(pos) => { + Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs <= rhs, *pos); + context.pc += 1; + } - Instruction::GreaterEqualIntegers(pos) => { - Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs >= rhs, *pos); - context.pc += 1; - } + Instruction::GreaterIntegers(pos) => { + Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs > rhs, *pos); + context.pc += 1; + } - Instruction::EqualStrings(pos) => { - Machine::exec_equality_string_op2(context, |lhs, rhs| lhs == rhs, *pos); - context.pc += 1; - } + Instruction::GreaterEqualIntegers(pos) => { + Machine::exec_equality_integer_op2(context, |lhs, rhs| lhs >= rhs, *pos); + context.pc += 1; + } - Instruction::NotEqualStrings(pos) => { - Machine::exec_equality_string_op2(context, |lhs, rhs| lhs != rhs, *pos); - context.pc += 1; - } + Instruction::EqualStrings(pos) => { + Machine::exec_equality_string_op2(context, |lhs, rhs| lhs == rhs, *pos); + context.pc += 1; + } - Instruction::LessStrings(pos) => { - Machine::exec_equality_string_op2(context, |lhs, rhs| lhs < rhs, *pos); - context.pc += 1; - } + Instruction::NotEqualStrings(pos) => { + Machine::exec_equality_string_op2(context, |lhs, rhs| lhs != rhs, *pos); + context.pc += 1; + } - Instruction::LessEqualStrings(pos) => { - Machine::exec_equality_string_op2(context, |lhs, rhs| lhs <= rhs, *pos); - context.pc += 1; - } + Instruction::LessStrings(pos) => { + Machine::exec_equality_string_op2(context, |lhs, rhs| lhs < rhs, *pos); + context.pc += 1; + } - Instruction::GreaterStrings(pos) => { - Machine::exec_equality_string_op2(context, |lhs, rhs| lhs > rhs, *pos); - context.pc += 1; - } + Instruction::LessEqualStrings(pos) => { + Machine::exec_equality_string_op2(context, |lhs, rhs| lhs <= rhs, *pos); + context.pc += 1; + } - Instruction::GreaterEqualStrings(pos) => { - Machine::exec_equality_string_op2(context, |lhs, rhs| lhs >= rhs, *pos); - context.pc += 1; - } + Instruction::GreaterStrings(pos) => { + Machine::exec_equality_string_op2(context, |lhs, rhs| lhs > rhs, *pos); + context.pc += 1; + } - Instruction::AddDoubles(pos) => { - Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs + rhs, *pos); - context.pc += 1; - } + Instruction::GreaterEqualStrings(pos) => { + Machine::exec_equality_string_op2(context, |lhs, rhs| lhs >= rhs, *pos); + context.pc += 1; + } - Instruction::SubtractDoubles(pos) => { - Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs - rhs, *pos); - context.pc += 1; - } + Instruction::AddDoubles(pos) => { + Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs + rhs, *pos); + context.pc += 1; + } - Instruction::MultiplyDoubles(pos) => { - Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs * rhs, *pos); - context.pc += 1; - } + Instruction::SubtractDoubles(pos) => { + Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs - rhs, *pos); + context.pc += 1; + } - Instruction::DivideDoubles(pos) => { - Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs / rhs, *pos); - context.pc += 1; - } + Instruction::MultiplyDoubles(pos) => { + Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs * rhs, *pos); + context.pc += 1; + } - Instruction::ModuloDoubles(pos) => { - Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs % rhs, *pos); - context.pc += 1; - } + Instruction::DivideDoubles(pos) => { + Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs / rhs, *pos); + context.pc += 1; + } - Instruction::PowerDoubles(pos) => { - Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs.powf(rhs), *pos); - context.pc += 1; - } + Instruction::ModuloDoubles(pos) => { + Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs % rhs, *pos); + context.pc += 1; + } - Instruction::NegateDouble(pos) => { - Machine::exec_arithmetic_double_op1(context, |rhs| -rhs, *pos); - context.pc += 1; - } + Instruction::PowerDoubles(pos) => { + Machine::exec_arithmetic_double_op2(context, |lhs, rhs| lhs.powf(rhs), *pos); + context.pc += 1; + } - Instruction::AddIntegers(pos) => { - Machine::exec_arithmetic_integer_op2(context, value::add_integer, *pos)?; - context.pc += 1; - } + Instruction::NegateDouble(pos) => { + Machine::exec_arithmetic_double_op1(context, |rhs| -rhs, *pos); + context.pc += 1; + } - Instruction::SubtractIntegers(pos) => { - Machine::exec_arithmetic_integer_op2(context, value::sub_integer, *pos)?; - context.pc += 1; - } + Instruction::AddIntegers(pos) => { + Machine::exec_arithmetic_integer_op2(context, value::add_integer, *pos)?; + context.pc += 1; + } - Instruction::MultiplyIntegers(pos) => { - Machine::exec_arithmetic_integer_op2(context, value::mul_integer, *pos)?; - context.pc += 1; - } + Instruction::SubtractIntegers(pos) => { + Machine::exec_arithmetic_integer_op2(context, value::sub_integer, *pos)?; + context.pc += 1; + } - Instruction::DivideIntegers(pos) => { - Machine::exec_arithmetic_integer_op2(context, value::div_integer, *pos)?; - context.pc += 1; - } + Instruction::MultiplyIntegers(pos) => { + Machine::exec_arithmetic_integer_op2(context, value::mul_integer, *pos)?; + context.pc += 1; + } - Instruction::ModuloIntegers(pos) => { - Machine::exec_arithmetic_integer_op2(context, value::modulo_integer, *pos)?; - context.pc += 1; - } + Instruction::DivideIntegers(pos) => { + Machine::exec_arithmetic_integer_op2(context, value::div_integer, *pos)?; + context.pc += 1; + } - Instruction::PowerIntegers(pos) => { - Machine::exec_arithmetic_integer_op2(context, value::pow_integer, *pos)?; - context.pc += 1; - } + Instruction::ModuloIntegers(pos) => { + Machine::exec_arithmetic_integer_op2(context, value::modulo_integer, *pos)?; + context.pc += 1; + } - Instruction::NegateInteger(pos) => { - Machine::exec_arithmetic_integer_op1(context, value::neg_integer, *pos)?; - context.pc += 1; - } + Instruction::PowerIntegers(pos) => { + Machine::exec_arithmetic_integer_op2(context, value::pow_integer, *pos)?; + context.pc += 1; + } - Instruction::ConcatStrings(pos) => { - Machine::exec_arithmetic_string_op2(context, |lhs, rhs| lhs.to_owned() + rhs, *pos); - context.pc += 1; - } + Instruction::NegateInteger(pos) => { + Machine::exec_arithmetic_integer_op1(context, value::neg_integer, *pos)?; + context.pc += 1; + } - Instruction::Assign(key) => { - let (value, _pos) = context.value_stack.pop().unwrap(); - self.symbols.assign(key, value); - context.pc += 1; - } + Instruction::ConcatStrings(pos) => { + Machine::exec_arithmetic_string_op2( + context, + |lhs, rhs| lhs.to_owned() + rhs, + *pos, + ); + context.pc += 1; + } - Instruction::ArrayAssignment(name, vref_pos, nargs) => { - self.assign_array(context, name, *vref_pos, *nargs)?; - context.pc += 1; - } + Instruction::Assign(key) => { + let (value, _pos) = context.value_stack.pop().unwrap(); + self.symbols.assign(key, value); + context.pc += 1; + } - Instruction::ArrayLoad(name, pos, nargs) => { - self.array_ref(context, name, *pos, *nargs)?; - context.pc += 1; - } + Instruction::ArrayAssignment(name, vref_pos, nargs) => { + self.assign_array(context, name, *vref_pos, *nargs)?; + context.pc += 1; + } - Instruction::BuiltinCall(name, bref_pos, nargs) => { - self.builtin_call(context, name, *bref_pos, *nargs).await?; - context.pc += 1; - } + Instruction::ArrayLoad(name, pos, nargs) => { + self.array_ref(context, name, *pos, *nargs)?; + context.pc += 1; + } - Instruction::Call(span) => { - context.addr_stack.push(context.pc + 1); - context.pc = span.addr; - } + Instruction::BuiltinCall(name, bref_pos, nargs) => { + return Ok(InternalStopReason::Upcall(UpcallData { + name: name.clone(), + return_type: None, + pos: *bref_pos, + nargs: *nargs, + })); + } - Instruction::DoubleToInteger => { - let (d, pos) = context.value_stack.pop_double_with_pos(); - let i = - double_to_integer(d.round()).map_err(|e| Error::from_value_error(e, pos))?; - context.value_stack.push_integer(i, pos); - context.pc += 1; - } + Instruction::Call(span) => { + context.addr_stack.push(context.pc + 1); + context.pc = span.addr; + } - Instruction::FunctionCall(name, return_type, pos, nargs) => { - self.function_call(context, name, *return_type, *pos, *nargs).await?; - context.pc += 1; - } + Instruction::DoubleToInteger => { + let (d, pos) = context.value_stack.pop_double_with_pos(); + let i = double_to_integer(d.round()) + .map_err(|e| Error::from_value_error(e, pos))?; + context.value_stack.push_integer(i, pos); + context.pc += 1; + } - Instruction::Dim(span) => { - if span.shared { - self.symbols.dim_shared(span.name.clone(), span.vtype); - } else { - self.symbols.dim(span.name.clone(), span.vtype); + Instruction::FunctionCall(name, return_type, pos, nargs) => { + return Ok(InternalStopReason::Upcall(UpcallData { + name: name.clone(), + return_type: Some(*return_type), + pos: *pos, + nargs: *nargs, + })); } - context.pc += 1; - } - Instruction::DimArray(span) => { - self.dim_array(context, span)?; - context.pc += 1; - } + Instruction::Dim(span) => { + if span.shared { + self.symbols.dim_shared(span.name.clone(), span.vtype); + } else { + self.symbols.dim(span.name.clone(), span.vtype); + } + context.pc += 1; + } - Instruction::End(has_code) => { - self.end(context, *has_code)?; - } + Instruction::DimArray(span) => { + self.dim_array(context, span)?; + context.pc += 1; + } - Instruction::EnterScope => { - self.symbols.enter_scope(); - context.pc += 1; - } + Instruction::End(has_code) => { + context.pc += 1; + return self.end(context, *has_code); + } - Instruction::IntegerToDouble => { - let (i, pos) = context.value_stack.pop_integer_with_pos(); - context.value_stack.push_double(i as f64, pos); - context.pc += 1; - } + Instruction::EnterScope => { + self.symbols.enter_scope(); + context.pc += 1; + } - Instruction::Jump(span) => { - if span.addr <= context.pc { - self.check_stop = true; + Instruction::IntegerToDouble => { + let (i, pos) = context.value_stack.pop_integer_with_pos(); + context.value_stack.push_double(i as f64, pos); + context.pc += 1; } - context.pc = span.addr; - } - Instruction::JumpIfDefined(span) => { - if self.symbols.load(&span.var).is_some() { - if span.addr <= context.pc { - self.check_stop = true; - } + Instruction::Jump(span) => { + let old_pc = context.pc; context.pc = span.addr; - } else { - context.pc += 1; + if span.addr <= old_pc { + return Ok(InternalStopReason::CheckStop); + } } - } - Instruction::JumpIfTrue(addr) => { - let cond = context.value_stack.pop_boolean(); - if cond { - if *addr <= context.pc { - self.check_stop = true; + Instruction::JumpIfDefined(span) => { + if self.symbols.load(&span.var).is_some() { + let old_pc = context.pc; + context.pc = span.addr; + if span.addr <= old_pc { + return Ok(InternalStopReason::CheckStop); + } + } else { + context.pc += 1; } - context.pc = *addr; - } else { - context.pc += 1; } - } - Instruction::JumpIfNotTrue(addr) => { - let cond = context.value_stack.pop_boolean(); - if cond { - context.pc += 1; - } else { - if *addr <= context.pc { - self.check_stop = true; + Instruction::JumpIfTrue(addr) => { + let cond = context.value_stack.pop_boolean(); + if cond { + let old_pc = context.pc; + context.pc = *addr; + if *addr <= old_pc { + return Ok(InternalStopReason::CheckStop); + } + } else { + context.pc += 1; } - context.pc = *addr; } - } - Instruction::LeaveScope => { - self.symbols.leave_scope(); - context.pc += 1; - } + Instruction::JumpIfNotTrue(addr) => { + let cond = context.value_stack.pop_boolean(); + if cond { + context.pc += 1; + } else { + let old_pc = context.pc; + context.pc = *addr; + if *addr <= old_pc { + return Ok(InternalStopReason::CheckStop); + } + } + } - Instruction::LoadBoolean(key, pos) => { - let b = match self.load(key, *pos)? { - Value::Boolean(b) => b, - _ => unreachable!("Types are validated at compilation time"), - }; - context.value_stack.push_boolean(*b, *pos); - context.pc += 1; - } + Instruction::LeaveScope => { + self.symbols.leave_scope(); + context.pc += 1; + } - Instruction::LoadDouble(key, pos) => { - let d = match self.load(key, *pos)? { - Value::Double(d) => d, - _ => unreachable!("Types are validated at compilation time"), - }; - context.value_stack.push_double(*d, *pos); - context.pc += 1; - } + Instruction::LoadBoolean(key, pos) => { + let b = match self.load(key, *pos)? { + Value::Boolean(b) => b, + _ => unreachable!("Types are validated at compilation time"), + }; + context.value_stack.push_boolean(*b, *pos); + context.pc += 1; + } - Instruction::LoadInteger(key, pos) => { - let i = match self.load(key, *pos)? { - Value::Integer(i) => i, - _ => unreachable!("Types are validated at compilation time"), - }; - context.value_stack.push_integer(*i, *pos); - context.pc += 1; - } + Instruction::LoadDouble(key, pos) => { + let d = match self.load(key, *pos)? { + Value::Double(d) => d, + _ => unreachable!("Types are validated at compilation time"), + }; + context.value_stack.push_double(*d, *pos); + context.pc += 1; + } - Instruction::LoadString(key, pos) => { - let s = match self.load(key, *pos)? { - Value::Text(s) => s, - _ => unreachable!("Types are validated at compilation time"), - }; - context.value_stack.push_string(s.clone(), *pos); - context.pc += 1; - } + Instruction::LoadInteger(key, pos) => { + let i = match self.load(key, *pos)? { + Value::Integer(i) => i, + _ => unreachable!("Types are validated at compilation time"), + }; + context.value_stack.push_integer(*i, *pos); + context.pc += 1; + } - Instruction::LoadRef(key, etype, pos) => { - let sym = self.symbols.load(key); - if let Some(Symbol::Callable(f)) = sym { - if f.metadata().is_argless() { - let f = f.clone(); - self.argless_function_call(context, key, *etype, *pos, f).await?; - context.pc += 1; - return Ok(()); - } - }; + Instruction::LoadString(key, pos) => { + let s = match self.load(key, *pos)? { + Value::Text(s) => s, + _ => unreachable!("Types are validated at compilation time"), + }; + context.value_stack.push_string(s.clone(), *pos); + context.pc += 1; + } - context.value_stack.push_varref(key.clone(), *etype, *pos); - context.pc += 1; - } + Instruction::LoadRef(key, etype, pos) => { + let sym = self.symbols.load(key); + if let Some(Symbol::Callable(f)) = sym { + if f.metadata().is_argless() { + return Ok(InternalStopReason::Upcall(UpcallData { + name: key.clone(), + return_type: Some(*etype), + pos: *pos, + nargs: 0, + })); + } + }; - Instruction::Nop => { - context.pc += 1; - } + context.value_stack.push_varref(key.clone(), *etype, *pos); + context.pc += 1; + } - Instruction::PushBoolean(value, pos) => { - context.value_stack.push((Value::Boolean(*value), *pos)); - context.pc += 1; - } + Instruction::Nop => { + context.pc += 1; + } - Instruction::PushDouble(value, pos) => { - context.value_stack.push((Value::Double(*value), *pos)); - context.pc += 1; - } + Instruction::PushBoolean(value, pos) => { + context.value_stack.push((Value::Boolean(*value), *pos)); + context.pc += 1; + } - Instruction::PushInteger(value, pos) => { - context.value_stack.push((Value::Integer(*value), *pos)); - context.pc += 1; - } + Instruction::PushDouble(value, pos) => { + context.value_stack.push((Value::Double(*value), *pos)); + context.pc += 1; + } - Instruction::PushString(value, pos) => { - context.value_stack.push((Value::Text(value.clone()), *pos)); - context.pc += 1; - } + Instruction::PushInteger(value, pos) => { + context.value_stack.push((Value::Integer(*value), *pos)); + context.pc += 1; + } - Instruction::Return(pos) => match context.addr_stack.pop() { - Some(addr) => { - self.check_stop = true; - context.pc = addr + Instruction::PushString(value, pos) => { + context.value_stack.push((Value::Text(value.clone()), *pos)); + context.pc += 1; } - None => return new_syntax_error(*pos, "No address to return to".to_owned()), - }, - Instruction::SetErrorHandler(span) => { - context.err_handler = *span; - context.pc += 1; - } + Instruction::Return(pos) => match context.addr_stack.pop() { + Some(addr) => { + context.pc = addr; + return Ok(InternalStopReason::CheckStop); + } + None => return new_syntax_error(*pos, "No address to return to".to_owned()), + }, - Instruction::Unset(span) => { - self.symbols.unset(&span.name).expect("Should only unset variables that were set"); - context.pc += 1; + Instruction::SetErrorHandler(span) => { + context.err_handler = *span; + context.pc += 1; + } + + Instruction::Unset(span) => { + self.symbols + .unset(&span.name) + .expect("Should only unset variables that were set"); + context.pc += 1; + } } } - Ok(()) + Ok(InternalStopReason::Eof) } - /// Executes a single instruction in `instrs` as pointed to by the program counter in `context`. - async fn exec_one(&mut self, context: &mut Context, instrs: &[Instruction]) -> Result<()> { - let mut result = self.exec_safe(context, instrs).await; - if let Err(e) = result.as_ref() { - if e.is_catchable() { - self.last_error = Some(format!("{}", e)); + /// Handles the given error `e` according to the current error handler previously set by + /// `ON ERROR`. If the error can be handled gracefully, returns `Ok`; otherwise, returns the + /// input error unmodified. + fn handle_error( + &mut self, + instrs: &[Instruction], + context: &mut Context, + e: Error, + ) -> Result<()> { + if !e.is_catchable() { + return Err(e); + } + + self.last_error = Some(format!("{}", e)); - match context.err_handler { - ErrorHandlerISpan::Jump(addr) => { - context.pc = addr; - result = Ok(()); - } - ErrorHandlerISpan::None => (), - ErrorHandlerISpan::ResumeNext => { - if instrs[context.pc].is_statement() { + match context.err_handler { + ErrorHandlerISpan::Jump(addr) => { + context.pc = addr; + Ok(()) + } + ErrorHandlerISpan::None => Err(e), + ErrorHandlerISpan::ResumeNext => { + if instrs[context.pc].is_statement() { + context.pc += 1; + } else { + loop { + context.pc += 1; + if context.pc >= instrs.len() { + break; + } else if instrs[context.pc].is_statement() { context.pc += 1; - } else { - loop { - context.pc += 1; - if context.pc >= instrs.len() { - break; - } else if instrs[context.pc].is_statement() { - context.pc += 1; - break; - } - } + break; } - result = Ok(()); } } + Ok(()) } } - result + } + + /// Executes the instructions given in `instr`. + /// + /// This is a helper to `exec`, which prepares the machine with the program's data upfront. + async fn exec_with_data(&mut self, instrs: &[Instruction]) -> Result { + let mut context = Context::default(); + while context.pc < instrs.len() { + match self.exec_until_stop(&mut context, instrs) { + Ok(InternalStopReason::CheckStop) => { + if self.should_stop().await { + return Ok(StopReason::Break); + } + } + + Ok(InternalStopReason::Upcall(data)) => { + let result; + if let Some(return_type) = data.return_type { + result = self + .function_call( + &mut context, + &data.name, + return_type, + data.pos, + data.nargs, + ) + .await; + } else { + result = + self.builtin_call(&mut context, &data.name, data.pos, data.nargs).await; + } + match result { + Ok(()) => context.pc += 1, + Err(e) => self.handle_error(instrs, &mut context, e)?, + } + } + + Ok(InternalStopReason::Eof) => { + return Ok(StopReason::Eof); + } + + Ok(InternalStopReason::Exited(code)) => { + return Ok(StopReason::Exited(code)); + } + + Err(e) => self.handle_error(instrs, &mut context, e)?, + } + } + Ok(StopReason::Eof) } /// Executes a program extracted from the `input` readable. @@ -1464,33 +1578,13 @@ impl Machine { /// Note that this does not consume `self`. As a result, it is possible to execute multiple /// different programs on the same machine, all sharing state. pub async fn exec(&mut self, input: &mut dyn io::Read) -> Result { - debug_assert!(!self.check_stop); - debug_assert!(self.stop_reason.is_none()); - let image = compiler::compile(input, &self.symbols)?; assert!(self.data.is_empty()); self.data = image.data; - - let mut context = Context::default(); - let mut result = Ok(()); - while result.is_ok() && context.pc < image.instrs.len() { - debug_assert!(self.stop_reason.is_none() || self.check_stop); - if self.check_stop && image.instrs[context.pc].is_statement() { - if self.should_stop().await { - break; - } - self.check_stop = false; - } - - result = self.exec_one(&mut context, &image.instrs).await; - } - + let result = self.exec_with_data(&image.instrs).await; self.data.clear(); - result?; - - self.check_stop = false; - Ok(self.stop_reason.take().unwrap_or(StopReason::Eof)) + result } } From 4c2ab990359c3bf0dabd88dc2eb3b36e047ed164 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 15 Dec 2024 09:39:55 -0800 Subject: [PATCH 015/110] Appease new Clippy warnings with Rust 1.83.0 --- core/src/exec.rs | 2 +- core/src/lexer.rs | 2 +- core/src/parser.rs | 2 +- core/src/reader.rs | 2 +- rpi/src/lcd/font8.rs | 2 +- std/src/console/linebuffer.rs | 2 +- std/src/gpio/fakes.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/exec.rs b/core/src/exec.rs index efb21022..d3072780 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -359,7 +359,7 @@ pub struct Scope<'s> { fref_pos: LineCol, } -impl<'s> Drop for Scope<'s> { +impl Drop for Scope<'_> { fn drop(&mut self) { self.stack.discard(self.nargs); } diff --git a/core/src/lexer.rs b/core/src/lexer.rs index d1a98e1d..e358f612 100644 --- a/core/src/lexer.rs +++ b/core/src/lexer.rs @@ -722,7 +722,7 @@ pub struct PeekableLexer<'a> { peeked: Option, } -impl<'a> PeekableLexer<'a> { +impl PeekableLexer<'_> { /// Reads the previously-peeked token. /// /// Because `peek` reports read errors, this assumes that the caller already handled those diff --git a/core/src/parser.rs b/core/src/parser.rs index 54631be5..b91131b2 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -1959,7 +1959,7 @@ pub(crate) struct StatementIter<'a> { parser: Parser<'a>, } -impl<'a> Iterator for StatementIter<'a> { +impl Iterator for StatementIter<'_> { type Item = Result; fn next(&mut self) -> Option { diff --git a/core/src/reader.rs b/core/src/reader.rs index 60f6194a..bfc8e10a 100644 --- a/core/src/reader.rs +++ b/core/src/reader.rs @@ -122,7 +122,7 @@ impl<'a> CharReader<'a> { } } -impl<'a> Iterator for CharReader<'a> { +impl Iterator for CharReader<'_> { type Item = io::Result; fn next(&mut self) -> Option { diff --git a/rpi/src/lcd/font8.rs b/rpi/src/lcd/font8.rs index a65159e0..5a1091bf 100644 --- a/rpi/src/lcd/font8.rs +++ b/rpi/src/lcd/font8.rs @@ -13,7 +13,7 @@ // License for the specific language governing permissions and limitations // under the License. -/** +/* ****************************************************************************** * @file Font8.c * @author MCD Application Team diff --git a/std/src/console/linebuffer.rs b/std/src/console/linebuffer.rs index 4f17b0e9..68f98dda 100644 --- a/std/src/console/linebuffer.rs +++ b/std/src/console/linebuffer.rs @@ -64,7 +64,7 @@ impl LineBuffer { /// Extracts a range of characters from this buffer. pub fn range(&self, start_pos: usize, end_pos: usize) -> String { - let count = if start_pos > end_pos { 0 } else { end_pos - start_pos }; + let count = end_pos.saturating_sub(start_pos); self.chars().skip(start_pos).take(count).collect() } diff --git a/std/src/gpio/fakes.rs b/std/src/gpio/fakes.rs index 676f20b6..b9298954 100644 --- a/std/src/gpio/fakes.rs +++ b/std/src/gpio/fakes.rs @@ -214,7 +214,7 @@ impl<'a> MockPins<'a> { } } -impl<'a> Pins for MockPins<'a> { +impl Pins for MockPins<'_> { fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()> { let datum = match mode { PinMode::In => MockOp::encode(pin, MockOp::SetupIn), From 1fcce5cc317e999aed6551269be995ed087bc53e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 1 Dec 2024 21:52:00 -0800 Subject: [PATCH 016/110] Parameterize the font in the BufferedLcd Add a new Font trait and make font8 derive it via a new Font8 type. Then, inject this into the BufferedLcd so that the LCD can display any other font. It doesn't make a ton of sense to parameterize this statically but there is no way to change the font at runtime right now, so I'm opting for this approach until the need to support dynamic fonts arises. --- rpi/src/lcd/buffered/mod.rs | 38 ++++++++++++++-------------- rpi/src/lcd/buffered/tests.rs | 11 +++++---- rpi/src/lcd/buffered/testutils.rs | 8 +++--- rpi/src/lcd/font8.rs | 41 +++++++++++++++++++------------ rpi/src/lcd/mod.rs | 14 ++++++++++- rpi/src/st7735s.rs | 5 ++-- 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/rpi/src/lcd/buffered/mod.rs b/rpi/src/lcd/buffered/mod.rs index 9dad3ab1..edce675b 100644 --- a/rpi/src/lcd/buffered/mod.rs +++ b/rpi/src/lcd/buffered/mod.rs @@ -15,8 +15,7 @@ //! Buffered implementation of the `RasterOps` for a hardware LCD. -use crate::lcd::font8; -use crate::lcd::{to_xy_size, AsByteSlice, Lcd, LcdSize, LcdXY}; +use crate::lcd::{to_xy_size, AsByteSlice, Font, Lcd, LcdSize, LcdXY}; use endbasic_std::console::drawing; use endbasic_std::console::graphics::{RasterInfo, RasterOps}; use endbasic_std::console::{CharsXY, PixelsXY, SizeInPixels, RGB}; @@ -28,14 +27,15 @@ mod tests; #[cfg(test)] mod testutils; -/// Implements buffering for a backing slow LCD `L`. +/// Implements buffering for a backing slow LCD `L` that renders text with the font `F`. /// /// All drawing operations are saved to a memory-backed framebuffer. If syncing is enabled, drawing /// primitives are flushed right away to the device; otherwise, they are applied to memory only /// until an explicit sync is requested. The framebuffer is also used to implement all pixel data /// reading. -pub(crate) struct BufferedLcd { +pub(crate) struct BufferedLcd { lcd: L, + font: F, fb: Vec, stride: usize, @@ -43,19 +43,19 @@ pub(crate) struct BufferedLcd { damage: Option<(LcdXY, LcdXY)>, size_pixels: LcdSize, - glyph_size: LcdSize, size_chars: CharsXY, draw_color: L::Pixel, row_buffer: Vec, } -impl BufferedLcd +impl BufferedLcd where L: Lcd, + F: Font, { /// Creates a new buffered LCD backed by `lcd`. - pub(crate) fn new(lcd: L) -> Self { + pub(crate) fn new(lcd: L, font: F) -> Self { let (size, stride) = lcd.info(); let fb = { @@ -63,7 +63,7 @@ where vec![0; pixels * stride] }; - let glyph_size = LcdSize { width: font8::WIDTH, height: font8::HEIGHT }; + let glyph_size = font.size(); let size_chars = CharsXY::new( u16::try_from(size.width / glyph_size.width).expect("Must fit"), u16::try_from(size.height / glyph_size.height).expect("Must fit"), @@ -74,12 +74,12 @@ where Self { lcd, + font, fb, stride, sync: true, damage: None, size_pixels: size, - glyph_size, size_chars, draw_color, row_buffer, @@ -87,9 +87,9 @@ where } /// Executes mutations on the buffered LCD via `ops` while ensuring that syncing is disabled. - fn without_sync(&mut self, ops: F) -> io::Result<()> + fn without_sync(&mut self, ops: O) -> io::Result<()> where - F: Fn(&mut BufferedLcd) -> io::Result<()>, + O: Fn(&mut BufferedLcd) -> io::Result<()>, { if self.sync { let old_sync = self.sync; @@ -329,9 +329,10 @@ where } } -impl Drop for BufferedLcd +impl Drop for BufferedLcd where L: Lcd, + F: Font, { fn drop(&mut self) { self.set_draw_color((0, 0, 0)); @@ -339,16 +340,17 @@ where } } -impl RasterOps for BufferedLcd +impl RasterOps for BufferedLcd where L: Lcd, + F: Font, { type ID = (Vec, SizeInPixels); fn get_info(&self) -> RasterInfo { RasterInfo { size_pixels: self.size_pixels.into(), - glyph_size: self.glyph_size.into(), + glyph_size: self.font.size().into(), size_chars: self.size_chars, } } @@ -453,11 +455,11 @@ where self.without_sync(|self2| { let mut pos = x1y1; for ch in text.chars() { - let glyph = font8::glyph(ch); - debug_assert_eq!(self2.glyph_size.height, glyph.len()); + let glyph = self2.font.glyph(ch); + debug_assert_eq!(self2.font.size().height, glyph.len()); for (j, row) in glyph.iter().enumerate() { let mut mask = 0x80; - for i in 0..self2.glyph_size.width { + for i in 0..self2.font.size().width { let bit = row & mask; if bit != 0 { let x = pos.x + i; @@ -477,7 +479,7 @@ where } } - pos.x += self2.glyph_size.width; + pos.x += self2.font.size().width; } Ok(()) }) diff --git a/rpi/src/lcd/buffered/tests.rs b/rpi/src/lcd/buffered/tests.rs index 9bda0c76..49e07352 100644 --- a/rpi/src/lcd/buffered/tests.rs +++ b/rpi/src/lcd/buffered/tests.rs @@ -17,6 +17,7 @@ use super::testutils::*; use super::*; +use crate::lcd::font8::Font8; use endbasic_std::console::graphics::RasterOps; use endbasic_std::console::{CharsXY, PixelsXY, SizeInPixels}; @@ -27,7 +28,7 @@ fn test_new_does_nothing() { #[test] fn test_clip_xy() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200))); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); assert_eq!(Some(xy(0, 0)), lcd.clip_xy(PixelsXY::new(0, 0))); assert_eq!(Some(xy(10, 20)), lcd.clip_xy(PixelsXY::new(10, 20))); @@ -41,7 +42,7 @@ fn test_clip_xy() { #[test] fn test_clamp_xy() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200))); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); assert_eq!(xy(0, 0), lcd.clamp_xy(PixelsXY::new(0, 0))); assert_eq!(xy(10, 20), lcd.clamp_xy(PixelsXY::new(10, 20))); @@ -55,7 +56,7 @@ fn test_clamp_xy() { #[test] fn test_clip_x2y2() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200))); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); assert_eq!(Some(xy(9, 19)), lcd.clip_x2y2(PixelsXY::new(0, 0), SizeInPixels::new(10, 20))); assert_eq!(Some(xy(19, 39)), lcd.clip_x2y2(PixelsXY::new(10, 20), SizeInPixels::new(10, 20))); @@ -79,7 +80,7 @@ fn test_clip_x2y2() { #[test] fn test_fb_addr() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200))); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); assert_eq!(0, lcd.fb_addr(0, 0)); assert_eq!(3, lcd.fb_addr(1, 0)); @@ -194,7 +195,7 @@ fn test_force_present_canvas_damage() { #[test] fn test_get_info() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200))); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); let info = lcd.get_info(); assert_eq!(info.size_pixels, SizeInPixels::new(100, 200)); assert_eq!(info.glyph_size, SizeInPixels::new(5, 8)); diff --git a/rpi/src/lcd/buffered/testutils.rs b/rpi/src/lcd/buffered/testutils.rs index 1ce57a38..35408c02 100644 --- a/rpi/src/lcd/buffered/testutils.rs +++ b/rpi/src/lcd/buffered/testutils.rs @@ -15,7 +15,7 @@ //! Utilities to implement tests for the `BufferedLcd`. -use crate::lcd::{BufferedLcd, Lcd, LcdSize, LcdXY, RGB888Pixel}; +use crate::lcd::{font8::Font8, BufferedLcd, Lcd, LcdSize, LcdXY, RGB888Pixel}; use endbasic_std::console::RGB; use std::io; @@ -68,7 +68,7 @@ impl Lcd for LcdRecorder { #[must_use] pub(super) struct Tester { size: LcdSize, - buffered: BufferedLcd, + buffered: BufferedLcd, exp_fb: Vec, exp_damage: Option<(LcdXY, LcdXY)>, exp_ops: Vec, @@ -80,7 +80,7 @@ impl Tester { let fb_size = size.width * size.height * 3; Self { size, - buffered: BufferedLcd::new(LcdRecorder::new(size)), + buffered: BufferedLcd::new(LcdRecorder::new(size), Font8::default()), exp_fb: vec![0; fb_size], exp_damage: None, exp_ops: vec![], @@ -90,7 +90,7 @@ impl Tester { /// Executes an operation on the backing `LcdRecorder`. pub(super) fn op<'a, F>(mut self, op: F) -> Self where - F: Fn(&mut BufferedLcd), + F: Fn(&mut BufferedLcd), { op(&mut self.buffered); self diff --git a/rpi/src/lcd/font8.rs b/rpi/src/lcd/font8.rs index 5a1091bf..ded468ab 100644 --- a/rpi/src/lcd/font8.rs +++ b/rpi/src/lcd/font8.rs @@ -49,12 +49,13 @@ * ****************************************************************************** */ +use crate::lcd::{Font, LcdSize}; /// Width of the font glyphs in pixels. -pub(crate) const WIDTH: usize = 5; +const WIDTH: usize = 5; /// Height of the font glyphs in pixels. -pub(crate) const HEIGHT: usize = 8; +const HEIGHT: usize = 8; /// Raw font data table for ASCII characters. Use `glyph` to access. const DATA: &[u8] = &[ @@ -915,19 +916,25 @@ const DATA: &[u8] = &[ 0x00, // ]; -/// Returns the pixel data for the given `ch`. -/// -/// The returned slice contains one byte per row, and each row indicates which pixels need to be -/// drawn from left to right. Only the first `WIDTH` bits in every row contain valid data. -pub(crate) fn glyph(mut ch: char) -> &'static [u8] { - if !(' '..='~').contains(&ch) { - // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode characters - // are typically displayed. - ch = '?'; +/// A small font to be used in small LCDs. +#[derive(Default)] +pub(crate) struct Font8 {} + +impl Font for Font8 { + fn size(&self) -> LcdSize { + LcdSize { width: WIDTH, height: HEIGHT } + } + + fn glyph(&self, mut ch: char) -> &'static [u8] { + if !(' '..='~').contains(&ch) { + // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode + // characters are typically displayed. + ch = '?'; + } + let offset = ((ch as usize) - (' ' as usize)) * HEIGHT; + debug_assert!(offset < (DATA.len() + HEIGHT)); + &DATA[offset..offset + HEIGHT] } - let offset = ((ch as usize) - (' ' as usize)) * HEIGHT; - debug_assert!(offset < (DATA.len() + HEIGHT)); - &DATA[offset..offset + HEIGHT] } #[cfg(test)] @@ -939,7 +946,8 @@ mod tests { let offset = (usize::from(b'a') - usize::from(b' ')) * 8; let expected = &DATA[offset..offset + 8]; - let data = glyph('a'); + let font = Font8::default(); + let data = font.glyph('a'); assert_eq!(expected, data); } @@ -948,7 +956,8 @@ mod tests { let offset = (usize::from(b'?') - usize::from(b' ')) * 8; let expected = &DATA[offset..offset + 8]; - let data = glyph(char::from(30)); + let font = Font8::default(); + let data = font.glyph(char::from(30)); assert_eq!(expected, data); } } diff --git a/rpi/src/lcd/mod.rs b/rpi/src/lcd/mod.rs index 8959b33b..fc5fabf7 100644 --- a/rpi/src/lcd/mod.rs +++ b/rpi/src/lcd/mod.rs @@ -20,7 +20,7 @@ use std::convert::TryFrom; use std::io; mod buffered; -mod font8; +pub(crate) mod font8; pub(crate) use buffered::BufferedLcd; @@ -50,6 +50,18 @@ impl AsByteSlice for RGB888Pixel { } } +/// Representation of a font. +pub(crate) trait Font { + /// Returns the size of a glyph, in pixels. + fn size(&self) -> LcdSize; + + /// Returns the pixel data for the given `ch`. + /// + /// The returned slice contains one byte per row, and each row indicates which pixels need to be + /// drawn from left to right. Only the first `WIDTH` bits in every row contain valid data. + fn glyph(&self, ch: char) -> &'static [u8]; +} + /// Primitives that an LCD must define. pub(crate) trait Lcd { /// The primitive type of the pixel data. diff --git a/rpi/src/st7735s.rs b/rpi/src/st7735s.rs index 6d875fa5..e34b555c 100644 --- a/rpi/src/st7735s.rs +++ b/rpi/src/st7735s.rs @@ -24,6 +24,7 @@ //! Console driver for the ST7735S LCD. use crate::gpio::gpio_error_to_io_error; +use crate::lcd::font8::Font8; use crate::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel}; use async_channel::Sender; use async_trait::async_trait; @@ -363,7 +364,7 @@ pub struct ST7735SConsole { /// The graphical console itself. We wrap it in a struct to prevent leaking all auxiliary types /// outside of this crate. - inner: GraphicsConsole>, + inner: GraphicsConsole>, } #[async_trait(?Send)] @@ -471,7 +472,7 @@ pub fn new_st7735s_console(signals_tx: Sender) -> io::Result Date: Fri, 29 Nov 2024 17:44:37 -0800 Subject: [PATCH 017/110] Turn std::gfx into a subdirectory module --- std/src/{gfx.rs => gfx/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename std/src/{gfx.rs => gfx/mod.rs} (100%) diff --git a/std/src/gfx.rs b/std/src/gfx/mod.rs similarity index 100% rename from std/src/gfx.rs rename to std/src/gfx/mod.rs From 4b9d0c0b82fb8eb4d7e60564d82f7186b5ff0399 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 2 Dec 2024 16:21:41 -0800 Subject: [PATCH 018/110] Move LCD code into std::gfx --- rpi/src/lib.rs | 1 - rpi/src/st7735s.rs | 3 +- {rpi/src => std/src/gfx}/lcd/buffered/mod.rs | 12 ++--- .../src => std/src/gfx}/lcd/buffered/tests.rs | 6 +-- .../src/gfx}/lcd/buffered/testutils.rs | 18 +++++-- {rpi/src => std/src/gfx}/lcd/font8.rs | 7 ++- {rpi/src => std/src/gfx}/lcd/mod.rs | 49 +++++++++---------- std/src/gfx/mod.rs | 2 + 8 files changed, 55 insertions(+), 43 deletions(-) rename {rpi/src => std/src/gfx}/lcd/buffered/mod.rs (98%) rename {rpi/src => std/src/gfx}/lcd/buffered/tests.rs (99%) rename {rpi/src => std/src/gfx}/lcd/buffered/testutils.rs (93%) rename {rpi/src => std/src/gfx}/lcd/font8.rs (99%) rename {rpi/src => std/src/gfx}/lcd/mod.rs (86%) diff --git a/rpi/src/lib.rs b/rpi/src/lib.rs index 2d21232d..82b5b7f5 100644 --- a/rpi/src/lib.rs +++ b/rpi/src/lib.rs @@ -23,6 +23,5 @@ mod gpio; pub use gpio::RppalPins; -pub mod lcd; mod st7735s; pub use st7735s::new_st7735s_console; diff --git a/rpi/src/st7735s.rs b/rpi/src/st7735s.rs index e34b555c..d1efaa04 100644 --- a/rpi/src/st7735s.rs +++ b/rpi/src/st7735s.rs @@ -24,8 +24,6 @@ //! Console driver for the ST7735S LCD. use crate::gpio::gpio_error_to_io_error; -use crate::lcd::font8::Font8; -use crate::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel}; use async_channel::Sender; use async_trait::async_trait; use endbasic_core::exec::Signal; @@ -33,6 +31,7 @@ use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, }; +use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY, RGB565Pixel}; use endbasic_terminal::TerminalConsole; use rppal::gpio::{Gpio, InputPin, Level, OutputPin}; use rppal::spi::{self, Bus, SlaveSelect, Spi}; diff --git a/rpi/src/lcd/buffered/mod.rs b/std/src/gfx/lcd/buffered/mod.rs similarity index 98% rename from rpi/src/lcd/buffered/mod.rs rename to std/src/gfx/lcd/buffered/mod.rs index edce675b..fa5c7542 100644 --- a/rpi/src/lcd/buffered/mod.rs +++ b/std/src/gfx/lcd/buffered/mod.rs @@ -15,10 +15,10 @@ //! Buffered implementation of the `RasterOps` for a hardware LCD. -use crate::lcd::{to_xy_size, AsByteSlice, Font, Lcd, LcdSize, LcdXY}; -use endbasic_std::console::drawing; -use endbasic_std::console::graphics::{RasterInfo, RasterOps}; -use endbasic_std::console::{CharsXY, PixelsXY, SizeInPixels, RGB}; +use crate::console::drawing; +use crate::console::graphics::{RasterInfo, RasterOps}; +use crate::console::{CharsXY, PixelsXY, SizeInPixels, RGB}; +use crate::gfx::lcd::{to_xy_size, AsByteSlice, Font, Lcd, LcdSize, LcdXY}; use std::convert::TryFrom; use std::io; @@ -33,7 +33,7 @@ mod testutils; /// primitives are flushed right away to the device; otherwise, they are applied to memory only /// until an explicit sync is requested. The framebuffer is also used to implement all pixel data /// reading. -pub(crate) struct BufferedLcd { +pub struct BufferedLcd { lcd: L, font: F, @@ -55,7 +55,7 @@ where F: Font, { /// Creates a new buffered LCD backed by `lcd`. - pub(crate) fn new(lcd: L, font: F) -> Self { + pub fn new(lcd: L, font: F) -> Self { let (size, stride) = lcd.info(); let fb = { diff --git a/rpi/src/lcd/buffered/tests.rs b/std/src/gfx/lcd/buffered/tests.rs similarity index 99% rename from rpi/src/lcd/buffered/tests.rs rename to std/src/gfx/lcd/buffered/tests.rs index 49e07352..12f9f44e 100644 --- a/rpi/src/lcd/buffered/tests.rs +++ b/std/src/gfx/lcd/buffered/tests.rs @@ -17,9 +17,9 @@ use super::testutils::*; use super::*; -use crate::lcd::font8::Font8; -use endbasic_std::console::graphics::RasterOps; -use endbasic_std::console::{CharsXY, PixelsXY, SizeInPixels}; +use crate::console::graphics::RasterOps; +use crate::console::{CharsXY, PixelsXY, SizeInPixels}; +use crate::gfx::lcd::Font8; #[test] fn test_new_does_nothing() { diff --git a/rpi/src/lcd/buffered/testutils.rs b/std/src/gfx/lcd/buffered/testutils.rs similarity index 93% rename from rpi/src/lcd/buffered/testutils.rs rename to std/src/gfx/lcd/buffered/testutils.rs index 35408c02..7ce107a6 100644 --- a/rpi/src/lcd/buffered/testutils.rs +++ b/std/src/gfx/lcd/buffered/testutils.rs @@ -15,10 +15,22 @@ //! Utilities to implement tests for the `BufferedLcd`. -use crate::lcd::{font8::Font8, BufferedLcd, Lcd, LcdSize, LcdXY, RGB888Pixel}; -use endbasic_std::console::RGB; +use crate::console::RGB; +use crate::gfx::lcd::{AsByteSlice, BufferedLcd, Font8, Lcd, LcdSize, LcdXY}; use std::io; +/// Data for one pixel encoded as RGB888. +#[cfg(test)] +#[derive(Clone, Copy)] +pub(super) struct RGB888Pixel(pub(super) [u8; 3]); + +#[cfg(test)] +impl AsByteSlice for RGB888Pixel { + fn as_slice(&self) -> &[u8] { + &self.0 + } +} + /// Syntactic sugar to instantiate a coordinate in the LCD space. pub(super) fn xy(x: usize, y: usize) -> LcdXY { LcdXY { x, y } @@ -88,7 +100,7 @@ impl Tester { } /// Executes an operation on the backing `LcdRecorder`. - pub(super) fn op<'a, F>(mut self, op: F) -> Self + pub(super) fn op(mut self, op: F) -> Self where F: Fn(&mut BufferedLcd), { diff --git a/rpi/src/lcd/font8.rs b/std/src/gfx/lcd/font8.rs similarity index 99% rename from rpi/src/lcd/font8.rs rename to std/src/gfx/lcd/font8.rs index ded468ab..eecf9fb7 100644 --- a/rpi/src/lcd/font8.rs +++ b/std/src/gfx/lcd/font8.rs @@ -49,7 +49,10 @@ * ****************************************************************************** */ -use crate::lcd::{Font, LcdSize}; + +//! Small font for tiny displays. + +use crate::gfx::lcd::{Font, LcdSize}; /// Width of the font glyphs in pixels. const WIDTH: usize = 5; @@ -918,7 +921,7 @@ const DATA: &[u8] = &[ /// A small font to be used in small LCDs. #[derive(Default)] -pub(crate) struct Font8 {} +pub struct Font8 {} impl Font for Font8 { fn size(&self) -> LcdSize { diff --git a/rpi/src/lcd/mod.rs b/std/src/gfx/lcd/mod.rs similarity index 86% rename from rpi/src/lcd/mod.rs rename to std/src/gfx/lcd/mod.rs index fc5fabf7..e4ea98eb 100644 --- a/rpi/src/lcd/mod.rs +++ b/std/src/gfx/lcd/mod.rs @@ -15,22 +15,25 @@ //! Generic types to represent and manipulate LCDs. -use endbasic_std::console::{SizeInPixels, RGB}; +use crate::console::{SizeInPixels, RGB}; use std::convert::TryFrom; use std::io; mod buffered; -pub(crate) mod font8; +mod font8; -pub(crate) use buffered::BufferedLcd; +pub use buffered::BufferedLcd; +pub use font8::Font8; -pub(crate) trait AsByteSlice { +/// Trait to convert a pixel to a sequence of bytes. +pub trait AsByteSlice { + /// Returns the byte representation of a pixel. fn as_slice(&self) -> &[u8]; } /// Data for one pixel encoded as RGB565. #[derive(Clone, Copy)] -pub(crate) struct RGB565Pixel(pub(crate) [u8; 2]); +pub struct RGB565Pixel(pub [u8; 2]); impl AsByteSlice for RGB565Pixel { fn as_slice(&self) -> &[u8] { @@ -38,20 +41,8 @@ impl AsByteSlice for RGB565Pixel { } } -/// Data for one pixel encoded as RGB888. -#[cfg(test)] -#[derive(Clone, Copy)] -pub(crate) struct RGB888Pixel(pub(crate) [u8; 3]); - -#[cfg(test)] -impl AsByteSlice for RGB888Pixel { - fn as_slice(&self) -> &[u8] { - &self.0 - } -} - /// Representation of a font. -pub(crate) trait Font { +pub trait Font { /// Returns the size of a glyph, in pixels. fn size(&self) -> LcdSize; @@ -63,7 +54,7 @@ pub(crate) trait Font { } /// Primitives that an LCD must define. -pub(crate) trait Lcd { +pub trait Lcd { /// The primitive type of the pixel data. type Pixel: AsByteSlice + Copy; @@ -81,17 +72,23 @@ pub(crate) trait Lcd { /// Represents valid coordinates within the LCD space. #[derive(Clone, Copy)] #[cfg_attr(test, derive(Debug, PartialEq))] -pub(crate) struct LcdXY { - pub(crate) x: usize, - pub(crate) y: usize, +pub struct LcdXY { + /// The X coordinate. + pub x: usize, + + /// The Y coordinate. + pub y: usize, } /// Represents a size that fits in the LCD space. #[derive(Clone, Copy)] #[cfg_attr(test, derive(Debug, PartialEq))] -pub(crate) struct LcdSize { - pub(crate) width: usize, - pub(crate) height: usize, +pub struct LcdSize { + /// The width. + pub width: usize, + + /// The height. + pub height: usize, } impl LcdSize { @@ -119,7 +116,7 @@ impl From for SizeInPixels { } /// Converts a pair of coordinates to a top-left origin coordinate plus a size. -pub(crate) fn to_xy_size(x1y1: LcdXY, x2y2: LcdXY) -> (LcdXY, LcdSize) { +pub fn to_xy_size(x1y1: LcdXY, x2y2: LcdXY) -> (LcdXY, LcdSize) { let x1 = std::cmp::min(x1y1.x, x2y2.x); let y1 = std::cmp::min(x1y1.y, x2y2.y); diff --git a/std/src/gfx/mod.rs b/std/src/gfx/mod.rs index 32f68f3e..abf7ac00 100644 --- a/std/src/gfx/mod.rs +++ b/std/src/gfx/mod.rs @@ -27,6 +27,8 @@ use std::cell::RefCell; use std::convert::TryFrom; use std::rc::Rc; +pub mod lcd; + /// Category description for all symbols provided by this module. const CATEGORY: &str = "Graphics The EndBASIC console overlays text and graphics in the same canvas. The consequence of this \ From e711eadc0409112b896c5191617fdd89ac942863 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 5 Dec 2024 19:37:19 -0800 Subject: [PATCH 019/110] Allow Key to be constructed in a static context Remove the String from Key::Unknown so that all Key variants can be instantiated in a static context. This will be useful to define static keymaps where some keycodes don't have a valid corresponding Key. --- repl/src/editor.rs | 4 ++-- rpi/src/st7735s.rs | 2 +- std/src/console/cmds.rs | 2 +- std/src/console/mod.rs | 8 ++++---- std/src/console/readline.rs | 4 ++-- terminal/src/lib.rs | 18 +++++++----------- web/src/input.rs | 4 ++-- 7 files changed, 19 insertions(+), 23 deletions(-) diff --git a/repl/src/editor.rs b/repl/src/editor.rs index 84ea276f..8f296cf0 100644 --- a/repl/src/editor.rs +++ b/repl/src/editor.rs @@ -425,7 +425,7 @@ impl Editor { } // TODO(jmmv): Should do something smarter with unknown keys. - Key::Unknown(_) => (), + Key::Unknown => (), } } @@ -837,7 +837,7 @@ mod tests { Key::PageUp, Key::PageDown, ] { - cb.add_input_keys(&[k.clone()]); + cb.add_input_keys(&[*k]); ob = ob.quick_refresh(linecol(0, 0), yx(0, 0)); } diff --git a/rpi/src/st7735s.rs b/rpi/src/st7735s.rs index d1efaa04..420ec731 100644 --- a/rpi/src/st7735s.rs +++ b/rpi/src/st7735s.rs @@ -100,7 +100,7 @@ impl ST7735SInput { tokio::task::spawn(async move { async fn read_button(pin: &InputPin, key: Key, tx: &Sender) { if pin.read() == Level::Low { - if let Err(e) = tx.send(key.clone()).await { + if let Err(e) = tx.send(key).await { eprintln!("Ignoring button {:?} due to error: {}", key, e); } } diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index 2213b8a6..d4aa272a 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -247,7 +247,7 @@ impl Callable for InKeyFunction { Some(Key::PageDown) => "PGDOWN".to_owned(), Some(Key::PageUp) => "PGUP".to_owned(), Some(Key::Tab) => "TAB".to_owned(), - Some(Key::Unknown(_)) => "".to_owned(), + Some(Key::Unknown) => "?".to_owned(), None => "".to_owned(), }; diff --git a/std/src/console/mod.rs b/std/src/console/mod.rs index 7762ed0f..24057d7a 100644 --- a/std/src/console/mod.rs +++ b/std/src/console/mod.rs @@ -45,7 +45,7 @@ mod linebuffer; pub use linebuffer::LineBuffer; /// Decoded key presses as returned by the console. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Key { /// The cursor down key. ArrowDown, @@ -98,8 +98,8 @@ pub enum Key { /// The Tab key. Tab, - /// An unknown character or sequence. The text describes what went wrong. - Unknown(String), + /// An unknown character or sequence. + Unknown, } /// Indicates what part of the console to clear on a `Console::clear()` call. @@ -380,7 +380,7 @@ fn line_to_keys(s: String) -> VecDeque { } else if !ch.is_control() { keys.push_back(Key::Char(ch)); } else { - keys.push_back(Key::Unknown(format!("{}", ch))); + keys.push_back(Key::Unknown); } } keys diff --git a/std/src/console/readline.rs b/std/src/console/readline.rs index e1bfdb79..d1672ce0 100644 --- a/std/src/console/readline.rs +++ b/std/src/console/readline.rs @@ -251,7 +251,7 @@ async fn read_line_interactive( } // TODO(jmmv): Should do something smarter with unknown keys. - Key::Unknown(_) => (), + Key::Unknown => (), } } @@ -294,7 +294,7 @@ async fn read_line_raw(console: &mut dyn Console) -> io::Result { Key::NewLine => break, Key::PageDown | Key::PageUp => (), Key::Tab => (), - Key::Unknown(bad_input) => line += &bad_input, + Key::Unknown => line.push('?'), } } Ok(line) diff --git a/terminal/src/lib.rs b/terminal/src/lib.rs index 62ab5f65..1b1db836 100644 --- a/terminal/src/lib.rs +++ b/terminal/src/lib.rs @@ -156,18 +156,16 @@ impl TerminalConsole { KeyCode::Char('p') if ev.modifiers == KeyModifiers::CONTROL => Key::ArrowUp, KeyCode::Char(ch) => Key::Char(ch), KeyCode::Enter => Key::NewLine, - _ => Key::Unknown(format!("{:?}", ev)), + _ => Key::Unknown, } } Ok(_) => { // Not a key event; ignore and try again. continue; } - Err(e) => { - // There is not much we can do if we get an error from crossterm. Try to funnel - // the error somehow to the caller so that we can display that something went - // wrong... and continue anyhow. - Key::Unknown(format!("{:?}", e)) + Err(_) => { + // There is not much we can do if we get an error from crossterm. + Key::Unknown } }; @@ -209,11 +207,9 @@ impl TerminalConsole { while !done { let key = match read_key_from_stdin(&mut buffer) { Ok(key) => key, - Err(e) => { - // There is not much we can do if we get an error from stdin. Try to funnel - // the error somehow to the caller so that we can display that something went - // wrong... and continue anyhow. - Key::Unknown(format!("{:?}", e)) + Err(_) => { + // There is not much we can do if we get an error from stdin. + Key::Unknown } }; diff --git a/web/src/input.rs b/web/src/input.rs index b526c07f..6ea34956 100644 --- a/web/src/input.rs +++ b/web/src/input.rs @@ -35,7 +35,7 @@ fn on_input_event_into_key(dom_event: InputEvent) -> Key { if chars.len() == 1 { Key::Char(chars[0]) } else { - Key::Unknown(format!("", chars)) + Key::Unknown } } @@ -71,7 +71,7 @@ fn on_key_event_into_key(dom_event: KeyboardEvent) -> Key { if printable && chars.len() == 1 { Key::Char(chars[0]) } else { - Key::Unknown(format!("", dom_event.key_code())) + Key::Unknown } } } From ca635bb6c0db0d093a2aa17950ba61faccbe965c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 20 Jan 2025 07:28:59 -0800 Subject: [PATCH 020/110] Add 2025 to general copyright notices --- NOTICE | 2 +- cli/NOTICE | 2 +- cli/src/main.rs | 2 +- client/NOTICE | 2 +- core/NOTICE | 2 +- repl/NOTICE | 2 +- repl/src/lib.rs | 2 +- rpi/NOTICE | 2 +- sdl/NOTICE | 2 +- std/NOTICE | 2 +- terminal/NOTICE | 2 +- web/NOTICE | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/NOTICE b/NOTICE index fccd5615..f03acae8 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/cli/NOTICE b/cli/NOTICE index fccd5615..f03acae8 100644 --- a/cli/NOTICE +++ b/cli/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/cli/src/main.rs b/cli/src/main.rs index 90a1b184..64421997 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -89,7 +89,7 @@ fn help(name: &str, opts: &Options) { /// Prints version information following the GNU Standards format. fn version() { println!("EndBASIC {}", env!("CARGO_PKG_VERSION")); - println!("Copyright 2020-2024 Julio Merino"); + println!("Copyright 2020-2025 Julio Merino"); println!("License Apache Version 2.0 "); } diff --git a/client/NOTICE b/client/NOTICE index fccd5615..f03acae8 100644 --- a/client/NOTICE +++ b/client/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/core/NOTICE b/core/NOTICE index fccd5615..f03acae8 100644 --- a/core/NOTICE +++ b/core/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/repl/NOTICE b/repl/NOTICE index fccd5615..f03acae8 100644 --- a/repl/NOTICE +++ b/repl/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 0fa257f3..7be20a7c 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -42,7 +42,7 @@ pub fn print_welcome(console: Rc>) -> io::Result<()> { } else { console.print("")?; console.print(&format!(" EndBASIC {}", env!("CARGO_PKG_VERSION")))?; - console.print(" Copyright 2020-2024 Julio Merino")?; + console.print(" Copyright 2020-2025 Julio Merino")?; console.print("")?; console.print(" Type HELP for interactive usage information.")?; } diff --git a/rpi/NOTICE b/rpi/NOTICE index cd58bdc7..2fd5c019 100644 --- a/rpi/NOTICE +++ b/rpi/NOTICE @@ -1,5 +1,5 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino ---- diff --git a/sdl/NOTICE b/sdl/NOTICE index fccd5615..f03acae8 100644 --- a/sdl/NOTICE +++ b/sdl/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/std/NOTICE b/std/NOTICE index fccd5615..f03acae8 100644 --- a/std/NOTICE +++ b/std/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/terminal/NOTICE b/terminal/NOTICE index fccd5615..f03acae8 100644 --- a/terminal/NOTICE +++ b/terminal/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino diff --git a/web/NOTICE b/web/NOTICE index fccd5615..f03acae8 100644 --- a/web/NOTICE +++ b/web/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2024 Julio Merino +Copyright 2020-2025 Julio Merino From 2d6f95e32fa7ea28579a5c996372b3b3381a370f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 20 Jan 2025 07:31:23 -0800 Subject: [PATCH 021/110] Pin GitHub workflow badges to the master branch I was surprised to see now, when visiting the project's page, that the release and test workflows were red. This was caused by me pushing a broken branch to the repo a month ago... and that should not be reflected in the "health" of the project in such a prominent place. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff959f55..b9c153a5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # The EndBASIC programming language -![Test](https://github.com/endbasic/endbasic/workflows/Test/badge.svg) -![Build release](https://github.com/endbasic/endbasic/workflows/Build%20release/badge.svg) -![Release checks](https://github.com/endbasic/endbasic/workflows/Release%20checks/badge.svg) -![Deploy to staging](https://github.com/endbasic/endbasic/workflows/Deploy%20to%20repl-staging.endbasic.dev/badge.svg) -![Deploy to release](https://github.com/endbasic/endbasic/workflows/Deploy%20to%20repl.endbasic.dev/badge.svg) +![Test](https://github.com/endbasic/endbasic/actions/workflows/test.yml/badge.svg?branch=master) +![Build release](https://github.com/endbasic/endbasic/actions/workflows/build.yml/badge.svg?branch=master) +![Release checks](https://github.com/endbasic/endbasic/actions/workflows/release.yml/badge.svg?branch=master) +![Deploy to staging](https://github.com/endbasic/endbasic/actions/workflows/deploy-staging.yml/badge.svg) +![Deploy to release](https://github.com/endbasic/endbasic/actions/workflows/deploy-release.yml/badge.svg) 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, From 2f88f3b286fa1ba882e76688fa102e25d49387a9 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 6 Feb 2025 19:34:01 -0800 Subject: [PATCH 022/110] Do not disable sync when printing help I thought it was a good idea to disable screens syncing while printing help messages, but it is really weird to not see any scrolling while the output is being generated. --- std/src/help.rs | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/std/src/help.rs b/std/src/help.rs index 8f9c1519..4f9a2b14 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -491,12 +491,10 @@ impl Callable for HelpCommand { if scope.nargs() == 0 { let mut console = self.console.borrow_mut(); - let previous = console.set_sync(false).map_err(|e| scope.io_error(e))?; let result = { let mut pager = Pager::new(&mut *console).map_err(|e| scope.io_error(e))?; self.summary(&topics, &mut pager).await }; - console.set_sync(previous).map_err(|e| scope.io_error(e))?; result.map_err(|e| scope.io_error(e))?; } else { debug_assert_eq!(1, scope.nargs()); @@ -504,12 +502,10 @@ impl Callable for HelpCommand { let topic = topics.find(&t, pos)?; let mut console = self.console.borrow_mut(); - let previous = console.set_sync(false).map_err(|e| scope.io_error(e))?; let result = { let mut pager = Pager::new(&mut *console).map_err(|e| scope.io_error(e))?; topic.describe(&mut pager).await }; - console.set_sync(previous).map_err(|e| scope.io_error(e))?; result.map_err(|e| scope.io_error(e))?; } @@ -722,10 +718,7 @@ This is the first and only topic with just one line. tester().add_callable(DoNothingCommand::new()).add_callable(EmptyFunction::new()); t.get_console().borrow_mut().set_color(Some(100), Some(200)).unwrap(); t.run("HELP") - .expect_output([ - CapturedOut::SetColor(Some(100), Some(200)), - CapturedOut::SetSync(false), - ]) + .expect_output([CapturedOut::SetColor(Some(100), Some(200))]) .expect_prints(header()) .expect_prints([""]) .expect_output([ @@ -760,7 +753,6 @@ This is the first and only topic with just one line. " Type END or press CTRL+D to exit.", "", ]) - .expect_output([CapturedOut::SetSync(true)]) .check(); } @@ -770,7 +762,7 @@ This is the first and only topic with just one line. tester().add_callable(DoNothingCommand::new()).add_callable(EmptyFunction::new()); t.get_console().borrow_mut().set_color(Some(70), Some(50)).unwrap(); t.run(r#"help "testing""#) - .expect_output([CapturedOut::SetColor(Some(70), Some(50)), CapturedOut::SetSync(false)]) + .expect_output([CapturedOut::SetColor(Some(70), Some(50))]) .expect_prints([""]) .expect_output([ CapturedOut::SetColor(Some(TITLE_COLOR), Some(50)), @@ -793,7 +785,6 @@ This is the first and only topic with just one line. CapturedOut::Print(" This is the blurb.".to_owned()), ]) .expect_prints(["", " Type HELP followed by the name of a topic for details.", ""]) - .expect_output([CapturedOut::SetSync(true)]) .check(); } @@ -802,7 +793,7 @@ This is the first and only topic with just one line. let mut t = tester().add_callable(DoNothingCommand::new()); t.get_console().borrow_mut().set_color(Some(20), Some(21)).unwrap(); t.run(r#"help "Do_Nothing""#) - .expect_output([CapturedOut::SetColor(Some(20), Some(21)), CapturedOut::SetSync(false)]) + .expect_output([CapturedOut::SetColor(Some(20), Some(21))]) .expect_prints([""]) .expect_output([ CapturedOut::SetColor(Some(TITLE_COLOR), Some(21)), @@ -818,7 +809,6 @@ This is the first and only topic with just one line. " Second paragraph of the extended description.", "", ]) - .expect_output([CapturedOut::SetSync(true)]) .check(); } @@ -826,7 +816,7 @@ This is the first and only topic with just one line. let mut t = tester().add_callable(EmptyFunction::new()); t.get_console().borrow_mut().set_color(Some(30), Some(26)).unwrap(); t.run(format!(r#"help "{}""#, name)) - .expect_output([CapturedOut::SetColor(Some(30), Some(26)), CapturedOut::SetSync(false)]) + .expect_output([CapturedOut::SetColor(Some(30), Some(26))]) .expect_prints([""]) .expect_output([ CapturedOut::SetColor(Some(TITLE_COLOR), Some(26)), @@ -842,7 +832,6 @@ This is the first and only topic with just one line. " Second paragraph of the extended description.", "", ]) - .expect_output([CapturedOut::SetSync(true)]) .check(); } @@ -861,7 +850,6 @@ This is the first and only topic with just one line. tester() .add_callable(DoNothingCommand::new()) .run(r#"topic = "Do_Nothing": HELP topic"#) - .expect_output([CapturedOut::SetSync(false)]) .expect_prints([""]) .expect_output([ CapturedOut::SetColor(Some(TITLE_COLOR), None), @@ -878,7 +866,6 @@ This is the first and only topic with just one line. "", ]) .expect_var("TOPIC", "Do_Nothing") - .expect_output([CapturedOut::SetSync(true)]) .check(); } @@ -891,7 +878,6 @@ This is the first and only topic with just one line. format!(" {} sample$", name) }; vec![ - CapturedOut::SetSync(false), CapturedOut::Print("".to_owned()), CapturedOut::SetColor(Some(TITLE_COLOR), None), CapturedOut::Print(spec), @@ -903,7 +889,6 @@ This is the first and only topic with just one line. CapturedOut::Print("".to_owned()), CapturedOut::Print(" Second paragraph of the extended description.".to_owned()), CapturedOut::Print("".to_owned()), - CapturedOut::SetSync(true), ] } @@ -993,10 +978,7 @@ This is the first and only topic with just one line. t.get_console().borrow_mut().add_input_keys(&[Key::NewLine]); t.get_console().borrow_mut().set_color(Some(100), Some(200)).unwrap(); t.run("HELP") - .expect_output([ - CapturedOut::SetColor(Some(100), Some(200)), - CapturedOut::SetSync(false), - ]) + .expect_output([CapturedOut::SetColor(Some(100), Some(200))]) .expect_prints(header()) .expect_prints([""]) .expect_output([ @@ -1039,7 +1021,6 @@ This is the first and only topic with just one line. ), CapturedOut::SetColor(Some(100), Some(200)), ]) - .expect_output([CapturedOut::SetSync(true)]) .check(); } } From ba1b893b143c7bcccb451c7249e3302f4cb7aade Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 18 Feb 2025 07:15:36 -0800 Subject: [PATCH 023/110] Document the st7735s driver in --help --- cli/src/main.rs | 3 +++ cli/tests/cli/help.out.rpi | 19 +++++++++++++++++++ cli/tests/cli/help.out.rpi.sdl | 25 +++++++++++++++++++++++++ cli/tests/integration_test.rs | 21 +++++++++++++++------ 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 cli/tests/cli/help.out.rpi create mode 100644 cli/tests/cli/help.out.rpi.sdl diff --git a/cli/src/main.rs b/cli/src/main.rs index 64421997..28eb5dd7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -80,6 +80,9 @@ fn help(name: &str, opts: &Options) { println!(" RESOLUTION can be one of 'fs' (for full screen),"); println!(" 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'"); } + if cfg!(feature = "rpi") { + println!(" st7735s enables the ST7735S LCD console"); + } println!(" text enables the text-based console"); println!(); println!("Report bugs to: https://github.com/endbasic/endbasic/issues"); diff --git a/cli/tests/cli/help.out.rpi b/cli/tests/cli/help.out.rpi new file mode 100644 index 00000000..d815db08 --- /dev/null +++ b/cli/tests/cli/help.out.rpi @@ -0,0 +1,19 @@ +Usage: endbasic [options] [program-file] + +Options: + --console CONSOLE-SPEC + type and properties of the console to use + -h, --help show command-line usage information and exit + -i, --interactive force interactive mode when running a script + --local-drive URI + location of the drive to mount as LOCAL + --service-url URL + base URL of the cloud service + --version show version information and exit + +CONSOLE-SPEC can be one of the following: + st7735s enables the ST7735S LCD console + text enables the text-based console + +Report bugs to: https://github.com/endbasic/endbasic/issues +EndBASIC home page: https://www.endbasic.dev/ diff --git a/cli/tests/cli/help.out.rpi.sdl b/cli/tests/cli/help.out.rpi.sdl new file mode 100644 index 00000000..a54c6243 --- /dev/null +++ b/cli/tests/cli/help.out.rpi.sdl @@ -0,0 +1,25 @@ +Usage: endbasic [options] [program-file] + +Options: + --console CONSOLE-SPEC + type and properties of the console to use + -h, --help show command-line usage information and exit + -i, --interactive force interactive mode when running a script + --local-drive URI + location of the drive to mount as LOCAL + --service-url URL + base URL of the cloud service + --version show version information and exit + +CONSOLE-SPEC can be one of the following: + graphics[:SPEC] enables the graphical console and configures it + with the settings in SPEC, which is of the form: + RESOLUTION,TTF_FONT_PATH,FONT_SIZE + individual components of the SPEC can be omitted + RESOLUTION can be one of 'fs' (for full screen), + 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' + st7735s enables the ST7735S LCD console + text enables the text-based console + +Report bugs to: https://github.com/endbasic/endbasic/issues +EndBASIC home page: https://www.endbasic.dev/ diff --git a/cli/tests/integration_test.rs b/cli/tests/integration_test.rs index 0b667979..bbbbffcb 100644 --- a/cli/tests/integration_test.rs +++ b/cli/tests/integration_test.rs @@ -203,12 +203,21 @@ fn test_cli_autoexec_is_ignored() { #[test] fn test_cli_help() { fn check_with_args(args: &[&str]) { - let exp_stdout = if cfg!(feature = "sdl") { - Behavior::File(src_path("cli/tests/cli/help.out.sdl")) - } else { - Behavior::File(src_path("cli/tests/cli/help.out")) - }; - check(bin_path("endbasic"), args, 0, Behavior::Null, exp_stdout, Behavior::Null); + let mut src = String::from("cli/tests/cli/help.out"); + if cfg!(feature = "rpi") { + src.push_str(".rpi"); + } + if cfg!(feature = "sdl") { + src.push_str(".sdl"); + } + check( + bin_path("endbasic"), + args, + 0, + Behavior::Null, + Behavior::File(src_path(&src)), + Behavior::Null, + ); } check_with_args(&["-h"]); check_with_args(&["--help"]); From 01ece27f3542b023d19e3c6334f1f1da1b8e8561 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 18 Feb 2025 07:08:41 -0800 Subject: [PATCH 024/110] Refactor the way we handle console specs Add a generic ConsoleSpec parser that can be used to progressively deconstruct the console specification given by the user. This is to allow each console driver to query its own parameters, without special-casing SDL. This is an incompatible change because the structure of the flag has changed. I'd have kept backwards compatibility, but this seems like a better design and the price is small. --- NEWS.md | 5 + cli/src/main.rs | 35 ++-- cli/tests/cli/help.out.rpi.sdl | 4 +- cli/tests/cli/help.out.sdl | 4 +- sdl/src/console.rs | 9 +- sdl/src/host.rs | 7 +- sdl/src/lib.rs | 62 ++++++- sdl/src/spec.rs | 254 -------------------------- std/Cargo.toml | 1 + std/src/console/mod.rs | 6 +- std/src/console/spec.rs | 314 +++++++++++++++++++++++++++++++++ 11 files changed, 407 insertions(+), 294 deletions(-) delete mode 100644 sdl/src/spec.rs create mode 100644 std/src/console/spec.rs diff --git a/NEWS.md b/NEWS.md index e60a52ce..4a175de1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -19,6 +19,11 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Improved the performance of the core interpreter by more than 2x. +* Modified the syntax of the `--console` driver. Graphical mode on the + desktop can now be enabled with `--console=sdl` (as opposed to + `--console=graphics`) and the optional parameters given to `sdl` are now of + the form `key=value` parameters. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/cli/src/main.rs b/cli/src/main.rs index 28eb5dd7..3c1933d7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -25,7 +25,7 @@ use anyhow::{anyhow, Result}; use async_channel::Sender; use endbasic_core::exec::Signal; -use endbasic_std::console::Console; +use endbasic_std::console::{Console, ConsoleSpec}; use endbasic_std::storage::Storage; use getopts::Options; use std::cell::RefCell; @@ -73,9 +73,9 @@ fn help(name: &str, opts: &Options) { println!("{}", opts.usage(&brief)); println!("CONSOLE-SPEC can be one of the following:"); if cfg!(feature = "sdl") { - println!(" graphics[:SPEC] enables the graphical console and configures it"); + println!(" sdl[:SPEC] enables the graphical console and configures it"); println!(" with the settings in SPEC, which is of the form:"); - println!(" RESOLUTION,TTF_FONT_PATH,FONT_SIZE"); + println!(" resolution=RESOLUTION,font_path=TTF,font_size=SIZE"); println!(" individual components of the SPEC can be omitted"); println!(" RESOLUTION can be one of 'fs' (for full screen),"); println!(" 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'"); @@ -190,18 +190,18 @@ fn setup_console( /// Creates the graphical console when SDL support is built in. #[cfg(feature = "sdl")] - pub fn setup_graphics_console( + pub fn setup_sdl_console( signals_tx: Sender, - spec: &str, + spec: &mut ConsoleSpec, ) -> io::Result>> { endbasic_sdl::setup(spec, signals_tx) } /// Errors out during the creation of the graphical console when SDL support is not compiled in. #[cfg(not(feature = "sdl"))] - pub fn setup_graphics_console( + pub fn setup_sdl_console( _signals_tx: Sender, - _spec: &str, + _spec: &mut ConsoleSpec, ) -> io::Result>> { // TODO(jmmv): Make this io::ErrorKind::Unsupported when our MSRV allows it. Err(io::Error::new(io::ErrorKind::InvalidInput, "SDL support not compiled in")) @@ -219,22 +219,21 @@ fn setup_console( Err(io::Error::new(io::ErrorKind::InvalidInput, "ST7735S support not compiled in")) } - let console: Rc> = match console_spec { - None | Some("text") => setup_text_console(signals_tx)?, - - Some("graphics") => setup_graphics_console(signals_tx, "")?, - Some("st7735s") => setup_st7735s_console(signals_tx)?, - Some(text) if text.starts_with("graphics:") => { - setup_graphics_console(signals_tx, &text["graphics:".len()..])? - } - - Some(text) => { + let mut console_spec = ConsoleSpec::init(console_spec.unwrap_or("text")); + let console: Rc> = match console_spec.driver { + "sdl" => setup_sdl_console(signals_tx, &mut console_spec)?, + "st7735s" => setup_st7735s_console(signals_tx)?, + "text" => setup_text_console(signals_tx)?, + driver => { return Err(io::Error::new( io::ErrorKind::InvalidInput, - format!("Invalid console spec {}", text), + format!("Unknown console driver {}", driver), )) } }; + console_spec.finish().map_err(|e| { + io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid --console flag: {}", e)) + })?; Ok(console) } diff --git a/cli/tests/cli/help.out.rpi.sdl b/cli/tests/cli/help.out.rpi.sdl index a54c6243..f25a8636 100644 --- a/cli/tests/cli/help.out.rpi.sdl +++ b/cli/tests/cli/help.out.rpi.sdl @@ -12,9 +12,9 @@ Options: --version show version information and exit CONSOLE-SPEC can be one of the following: - graphics[:SPEC] enables the graphical console and configures it + sdl[:SPEC] enables the graphical console and configures it with the settings in SPEC, which is of the form: - RESOLUTION,TTF_FONT_PATH,FONT_SIZE + resolution=RESOLUTION,font_path=TTF,font_size=SIZE individual components of the SPEC can be omitted RESOLUTION can be one of 'fs' (for full screen), 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' diff --git a/cli/tests/cli/help.out.sdl b/cli/tests/cli/help.out.sdl index 27025402..f1dafe54 100644 --- a/cli/tests/cli/help.out.sdl +++ b/cli/tests/cli/help.out.sdl @@ -12,9 +12,9 @@ Options: --version show version information and exit CONSOLE-SPEC can be one of the following: - graphics[:SPEC] enables the graphical console and configures it + sdl[:SPEC] enables the graphical console and configures it with the settings in SPEC, which is of the form: - RESOLUTION,TTF_FONT_PATH,FONT_SIZE + resolution=RESOLUTION,font_path=TTF,font_size=SIZE individual components of the SPEC can be omitted RESOLUTION can be one of 'fs' (for full screen), 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' diff --git a/sdl/src/console.rs b/sdl/src/console.rs index c42d0c74..6fac305c 100644 --- a/sdl/src/console.rs +++ b/sdl/src/console.rs @@ -16,12 +16,11 @@ //! Implementation of the EndBASIC console using SDL. use crate::host::{self, Request, Response}; -use crate::spec::Resolution; 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, SizeInPixels, + remove_control_chars, CharsXY, ClearType, Console, Key, PixelsXY, Resolution, SizeInPixels, }; use std::io; use std::path::PathBuf; @@ -245,6 +244,7 @@ mod testutils { use std::env; use std::fs::File; use std::io::{self, BufReader, Read}; + use std::num::NonZeroU32; use std::path::PathBuf; use std::sync::{Mutex, MutexGuard}; @@ -296,7 +296,10 @@ mod testutils { let lock = TEST_LOCK.lock().unwrap(); let signals_chan = async_channel::unbounded(); let console = SdlConsole::new( - Resolution::windowed(800, 600).unwrap(), + Resolution::Windowed(( + NonZeroU32::new(800).unwrap(), + NonZeroU32::new(600).unwrap(), + )), src_path("sdl/src/IBMPlexMono-Regular-6.0.0.ttf"), 16, signals_chan.0, diff --git a/sdl/src/host.rs b/sdl/src/host.rs index 2615ff20..755ad256 100644 --- a/sdl/src/host.rs +++ b/sdl/src/host.rs @@ -19,14 +19,13 @@ //! from a single thread. use crate::font::{font_error_to_io_error, MonospacedFont}; -use crate::spec::Resolution; 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, SizeInPixels, RGB, + CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, Resolution, SizeInPixels, RGB, }; use sdl2::event::Event; use sdl2::keyboard::{Keycode, Mod}; @@ -253,12 +252,12 @@ impl Context { window } Resolution::FullScreen(size) => { - let mut window = video.window(&title, size.0, size.1); + let mut window = video.window(&title, size.0.get(), size.1.get()); window.fullscreen(); window } Resolution::Windowed(size) => { - let mut window = video.window(&title, size.0, size.1); + let mut window = video.window(&title, size.0.get(), size.1.get()); window.position_centered(); window } diff --git a/sdl/src/lib.rs b/sdl/src/lib.rs index 343accce..d5719f95 100644 --- a/sdl/src/lib.rs +++ b/sdl/src/lib.rs @@ -24,33 +24,77 @@ use async_channel::Sender; use endbasic_core::exec::Signal; -use endbasic_std::console::Console; +use endbasic_std::console::{Console, ConsoleSpec, Resolution}; use std::cell::RefCell; -use std::io; +use std::fs::File; +use std::io::{self, Write}; +use std::num::NonZeroU32; +use std::path::PathBuf; use std::rc::Rc; +use tempfile::TempDir; mod console; mod font; mod host; -mod spec; + +/// Default resolution to use when none is provided. +const DEFAULT_RESOLUTION_PIXELS: (u32, u32) = (800, 600); + +/// Default font to use when none is provided. +const DEFAULT_FONT_BYTES: &[u8] = include_bytes!("IBMPlexMono-Regular-6.0.0.ttf"); + +/// Default font size. +const DEFAULT_FONT_SIZE: u16 = 16; /// Converts a flat string error message to an `io::Error`. fn string_error_to_io_error(e: String) -> io::Error { io::Error::new(io::ErrorKind::Other, e) } +/// Context to maintain a font on disk temporarily. +pub(crate) struct TempFont { + dir: TempDir, +} + +impl TempFont { + /// Gets an instance of the default font. + pub(crate) fn default_font() -> io::Result { + let dir = tempfile::tempdir()?; + let mut file = File::create(dir.path().join("font.ttf"))?; + file.write_all(DEFAULT_FONT_BYTES)?; + Ok(Self { dir }) + } + + /// Gets the path to the temporary font. + pub(crate) fn path(&self) -> PathBuf { + self.dir.path().join("font.ttf") + } +} + /// Creates the graphical console based on the given `spec`. -pub fn setup(spec: &str, signals_tx: Sender) -> io::Result>> { - let spec = spec::parse_graphics_spec(spec)?; - let console = match spec.1 { +pub fn setup( + spec: &mut ConsoleSpec, + signals_tx: Sender, +) -> io::Result>> { + let resolution: Resolution = spec.take_keyed_flag("resolution")?.unwrap_or_else(|| { + let width = NonZeroU32::new(DEFAULT_RESOLUTION_PIXELS.0).unwrap(); + let height = NonZeroU32::new(DEFAULT_RESOLUTION_PIXELS.1).unwrap(); + Resolution::Windowed((width, height)) + }); + + let font_path = spec.take_keyed_flag::("font_path")?; + + let font_size = spec.take_keyed_flag("font_size")?.unwrap_or(DEFAULT_FONT_SIZE); + + let console = match font_path { None => { - let default_font = spec::TempFont::default_font()?; - console::SdlConsole::new(spec.0, default_font.path(), spec.2, signals_tx)? + let default_font = TempFont::default_font()?; + console::SdlConsole::new(resolution, default_font.path(), font_size, signals_tx)? // The console has been created at this point, so it should be safe to drop // default_font and clean up the on-disk file backing it up. } Some(font_path) => { - console::SdlConsole::new(spec.0, font_path.to_owned(), spec.2, signals_tx)? + console::SdlConsole::new(resolution, font_path.to_owned(), font_size, signals_tx)? } }; Ok(Rc::from(RefCell::from(console))) diff --git a/sdl/src/spec.rs b/sdl/src/spec.rs deleted file mode 100644 index 9b2d9e97..00000000 --- a/sdl/src/spec.rs +++ /dev/null @@ -1,254 +0,0 @@ -// 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. - -//! Configuration support for the graphical console. - -use std::fs::File; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use tempfile::TempDir; - -/// Default resolution to use when none is provided. -const DEFAULT_RESOLUTION_PIXELS: (u32, u32) = (800, 600); - -/// Default font to use when none is provided. -const DEFAULT_FONT_BYTES: &[u8] = include_bytes!("IBMPlexMono-Regular-6.0.0.ttf"); - -/// Default font size. -const DEFAULT_FONT_SIZE: u16 = 16; - -/// Configures the resolution of the graphical console. -#[derive(Debug, PartialEq)] -pub(crate) enum Resolution { - /// Tells the console to start in full screen mode at the current desktop resolution. - FullScreenDesktop, - - /// Tells the console to start in full screen mode at the given resolution. - FullScreen((u32, u32)), - - /// Tells the console to start in windowed mode at the given resolution. - Windowed((u32, u32)), -} - -impl Resolution { - /// Ensures that the given resolution is valid to some extent. - fn validate_width_and_height(width: u32, height: u32) -> io::Result<()> { - if width == 0 { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Console width cannot be 0")); - } - if height == 0 { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Console height cannot be 0")); - } - Ok(()) - } - - /// Creates a new instance of this enum of type `FullScreen` after validating the parameters. - pub(crate) fn full_screen(width: u32, height: u32) -> io::Result { - Resolution::validate_width_and_height(width, height)?; - Ok(Self::FullScreen((width, height))) - } - - /// Creates a new instance of this enum of type `Windowed` after validating the parameters. - pub(crate) fn windowed(width: u32, height: u32) -> io::Result { - Resolution::validate_width_and_height(width, height)?; - Ok(Self::Windowed((width, height))) - } -} - -/// Returns the default resolution for the console. -fn default_resolution() -> Resolution { - Resolution::windowed(DEFAULT_RESOLUTION_PIXELS.0, DEFAULT_RESOLUTION_PIXELS.1) - .expect("Hardcoded default resolution must have been valid") -} - -/// Wrapper over `str::parse` to return `io::Result` with a custom `error` message. -fn parse_str(text: &str, error: &'static str) -> io::Result { - match text.parse::() { - Ok(value) => Ok(value), - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidInput, error)), - } -} - -/// Parses a graphical `resolution` of the form `WIDTHxHEIGHT[fs]` or `fs`. -fn parse_resolution(mut resolution: &str) -> io::Result { - if resolution == "fs" { - return Ok(Resolution::FullScreenDesktop); - } - - let fullscreen; - if resolution.ends_with("fs") { - resolution = resolution.strip_suffix("fs").expect("Suffix presence checked right above"); - fullscreen = true; - } else { - fullscreen = false; - } - - let resolution: Vec<&str> = resolution.split('x').collect(); - match resolution.as_slice() { - [width, height] => { - let width = parse_str(width, "Invalid width in resolution")?; - let height = parse_str(height, "Invalid height in resolution")?; - if fullscreen { - Ok(Resolution::full_screen(width, height)?) - } else { - Ok(Resolution::windowed(width, height)?) - } - } - _ => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid resolution format")), - } -} - -/// Parses a graphical console specification. -pub(crate) fn parse_graphics_spec(params: &str) -> io::Result<(Resolution, Option<&Path>, u16)> { - let invalid_spec = - Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid graphics console spec")); - - let mut params = params.split(','); - let resolution = match params.next() { - Some("") => default_resolution(), - Some(resolution) => parse_resolution(resolution)?, - None => return invalid_spec, - }; - let font_path = match params.next() { - Some("") => None, - Some(font_path) => Some(Path::new(font_path)), - None => None, - }; - let font_size = match params.next() { - Some("") => DEFAULT_FONT_SIZE, - Some(font_size) => parse_str(font_size, "Invalid font size")?, - None => DEFAULT_FONT_SIZE, - }; - if params.next().is_some() { - return invalid_spec; - } - - Ok((resolution, font_path, font_size)) -} - -/// Context to maintain a font on disk temporarily. -pub(crate) struct TempFont { - dir: TempDir, -} - -impl TempFont { - /// Gets an instance of the default font. - pub(crate) fn default_font() -> io::Result { - let dir = tempfile::tempdir()?; - let mut file = File::create(dir.path().join("font.ttf"))?; - file.write_all(DEFAULT_FONT_BYTES)?; - Ok(Self { dir }) - } - - /// Gets the path to the temporary font. - pub(crate) fn path(&self) -> PathBuf { - self.dir.path().join("font.ttf") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_resolution_full_screen_desktop() { - assert_eq!(Resolution::FullScreenDesktop, parse_resolution("fs").unwrap()); - } - - #[test] - fn test_parse_resolution_full_screen() { - assert_eq!( - Resolution::full_screen(123, 45).unwrap(), - parse_resolution("123x45fs").unwrap() - ); - } - - #[test] - fn test_parse_resolution_windowed() { - assert_eq!(Resolution::windowed(123, 45).unwrap(), parse_resolution("123x45").unwrap()); - } - - #[test] - fn test_parse_resolution_errors() { - fn check(exp_error: &str, s: &str) { - assert_eq!(exp_error, format!("{}", parse_resolution(s).unwrap_err())); - } - check("Invalid resolution format", "a"); - check("Invalid width in resolution", "1fsx2"); - check("Invalid height in resolution", "1x2f"); - check("Invalid width in resolution", "ax100"); - check("Invalid height in resolution", "100xa"); - check("Console width cannot be 0", "0x100"); - check("Console height cannot be 0", "100x0"); - } - - #[test] - fn test_parse_graphics_spec_empty() { - for spec in ["", ",", ",,"] { - let spec = parse_graphics_spec(spec).unwrap(); - assert_eq!(default_resolution(), spec.0); - assert_eq!(None, spec.1); - assert_eq!(DEFAULT_FONT_SIZE, spec.2); - } - } - - #[test] - fn test_parse_graphics_spec_only_resolution() { - for spec in ["1024x768", "1024x768,", "1024x768,,"] { - let spec = parse_graphics_spec(spec).unwrap(); - assert_eq!(Resolution::windowed(1024, 768).unwrap(), spec.0); - assert_eq!(None, spec.1); - assert_eq!(DEFAULT_FONT_SIZE, spec.2); - } - } - - #[test] - fn test_parse_graphics_spec_only_font_path() { - for spec in [",foo.ttf", ",foo.ttf,"] { - let spec = parse_graphics_spec(spec).unwrap(); - assert_eq!(default_resolution(), spec.0); - assert_eq!(Some(Path::new("foo.ttf")), spec.1); - assert_eq!(DEFAULT_FONT_SIZE, spec.2); - } - } - - #[test] - fn test_parse_graphics_spec_only_font_size() { - let spec = parse_graphics_spec(",,32").unwrap(); - assert_eq!(default_resolution(), spec.0); - assert_eq!(None, spec.1); - assert_eq!(32, spec.2); - } - - #[test] - fn test_parse_graphics_spec_full() { - let spec = parse_graphics_spec("1x2,/path/to/font.ttf,24").unwrap(); - assert_eq!(Resolution::windowed(1, 2).unwrap(), spec.0); - assert_eq!(Some(Path::new("/path/to/font.ttf")), spec.1); - assert_eq!(24, spec.2); - } - - #[test] - fn test_parse_graphics_spec_errors() { - fn check(exp_error: &str, s: &str) { - assert_eq!(exp_error, format!("{}", parse_graphics_spec(s).unwrap_err())); - } - check("Invalid graphics console spec", ",,,,"); - check("Invalid graphics console spec", "800x600,font.ttf,16,abc"); - check("Invalid resolution format", "a,font.ttf,16"); - check("Invalid font size", "100x200,font.ttf,a"); - } -} diff --git a/std/Cargo.toml b/std/Cargo.toml index b82dc2ee..e5f93bdf 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -16,6 +16,7 @@ async-channel = "2.2" async-trait = "0.1" futures-lite = "2.2" radix_trie = "0.2" +thiserror = "1.0" time = { version = "0.3", features = ["formatting", "local-offset", "std"] } [dependencies.endbasic-core] diff --git a/std/src/console/mod.rs b/std/src/console/mod.rs index 24057d7a..3cb0c9fa 100644 --- a/std/src/console/mod.rs +++ b/std/src/console/mod.rs @@ -35,14 +35,16 @@ pub(crate) use format::refill_and_page; pub use format::refill_and_print; pub mod graphics; pub use graphics::GraphicsConsole; +mod linebuffer; +pub use linebuffer::LineBuffer; mod pager; pub(crate) use pager::Pager; mod readline; pub use readline::{read_line, read_line_secure}; +mod spec; +pub use spec::{ConsoleSpec, Resolution}; mod trivial; pub use trivial::TrivialConsole; -mod linebuffer; -pub use linebuffer::LineBuffer; /// Decoded key presses as returned by the console. #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/std/src/console/spec.rs b/std/src/console/spec.rs new file mode 100644 index 00000000..90e1f066 --- /dev/null +++ b/std/src/console/spec.rs @@ -0,0 +1,314 @@ +// 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. + +//! Configuration support for the graphical console. + +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; +use std::io; +use std::num::NonZeroU32; +use std::str::FromStr; + +#[derive(Debug, thiserror::Error)] +#[error("{}", .0)] +pub struct ParseError(String); + +impl From for io::Error { + fn from(value: ParseError) -> Self { + Self::new(io::ErrorKind::InvalidInput, value.0) + } +} + +/// Syntactic sugar to create an error. +macro_rules! mkerror { + ($($arg:tt)*) => ({ + ParseError(format!($($arg)*)) + }) +} + +/// Result type for console specification parse errors. +type Result = std::result::Result; + +/// Representation of a screen resolution in pixels. +#[derive(Debug, PartialEq)] +pub enum Resolution { + /// Tells the console to start in full screen mode at the current desktop resolution. + FullScreenDesktop, + + /// Tells the console to start in full screen mode at the given resolution. + FullScreen((NonZeroU32, NonZeroU32)), + + /// Tells the console to start in windowed mode at the given resolution. + Windowed((NonZeroU32, NonZeroU32)), +} + +/// Parses a graphical `resolution` of the form `[WIDTHxHEIGHT][fs]`. +fn parse_resolution(resolution: &str) -> Result { + if resolution == "fs" { + return Ok(Resolution::FullScreenDesktop); + } + + let (dimensions, fullscreen) = match resolution.strip_suffix("fs") { + Some(prefix) => (prefix, true), + None => (resolution, false), + }; + + match dimensions.split_once('x') { + Some((width, height)) => { + let width = NonZeroU32::from_str(width).map_err(|e| { + mkerror!("Invalid width {} in resolution {}: {}", width, resolution, e) + })?; + let height = NonZeroU32::from_str(height).map_err(|e| { + mkerror!("Invalid height {} in resolution {}: {}", height, resolution, e) + })?; + + if fullscreen { + Ok(Resolution::FullScreen((width, height))) + } else { + Ok(Resolution::Windowed((width, height))) + } + } + _ => Err(mkerror!( + "Invalid resolution {}: must be of the form [WIDTHxHEIGHT][fs]", + resolution + )), + } +} + +impl FromStr for Resolution { + type Err = ParseError; + + fn from_str(s: &str) -> std::result::Result { + parse_resolution(s) + } +} + +/// Parser for a console specification. +/// +/// A console specification is a string of the form `driver[:flags]`. The optional flags are a +/// sequence of comma-separated flags where each flag can be a a boolean flag represented as a plain +/// string or keyed flag represented as a `key=value` string. +/// +/// The interface of this parser is designed to be instantiated from `main` based on a user-supplied +/// flag and then passed on to the specific console drivers for additional parsing. `main` then +/// needs to call `finish` to ensure that all provided flags have been parsed. +pub struct ConsoleSpec<'a> { + /// The name of the desired console driver. + pub driver: &'a str, + + /// Collection of boolean flags that appear in the input specification and that haven't been + /// queried yet. + flags: HashSet<&'a str>, + + /// Collection of keyed flags that appear in the input specification and that haven't been + /// queried yet. + keyed_flags: HashMap<&'a str, &'a str>, +} + +impl<'a> ConsoleSpec<'a> { + /// Initializes the console specification parser from `s`. + /// + /// `s` *must* not be empty. The caller must supply, at least, a default console driver name + /// if the user did not specify any console flag. + pub fn init(s: &'a str) -> Self { + assert!(!s.is_empty()); + + let (driver, rest) = s.split_once(':').unwrap_or((s, "")); + + let mut flags = HashSet::default(); + let mut keyed_flags = HashMap::default(); + for pair in rest.split(',') { + if pair.is_empty() { + continue; + } + + match pair.split_once('=') { + None => { + let _exists = flags.insert(pair); + } + Some((k, v)) => { + let _old = keyed_flags.insert(k, v); + } + } + } + + Self { driver, flags, keyed_flags } + } + + /// Queries whether the boolean `flag` is in the specification or not. + /// + /// The flag is marked as "checked" so that `finish` won't raise it as residual. + pub fn take_flag(&mut self, flag: &str) -> bool { + self.flags.remove(flag) + } + + fn take_keyed_flag_str(&mut self, key: &str) -> Option<&str> { + self.keyed_flags.remove(key) + } + + /// Queries the value of the keyed `flag` from the specification, which may or may not be + /// present. The value is parsed according to the type `V`. + /// + /// The flag is marked as "checked" so that `finish` won't raise it as residual. + pub fn take_keyed_flag(&mut self, key: &str) -> Result> + where + V: FromStr, + V::Err: Display, + { + match self.take_keyed_flag_str(key) { + Some(v) => V::from_str(v) + .map(|v| Some(v)) + .map_err(|e| mkerror!("Invalid console flag {}: {}", key, e)), + None => Ok(None), + } + } + + /// Validates that all provided flags have been queried by the driver. + pub fn finish(self) -> Result<()> { + if self.flags.is_empty() && self.keyed_flags.is_empty() { + Ok(()) + } else { + let flags_iter = self.flags.into_iter(); + let keyed_iter = self.keyed_flags.into_keys(); + let mut unknown = flags_iter.chain(keyed_iter).collect::>(); + unknown.sort(); + Err(mkerror!( + "Console driver {} does not recognize flags: {}", + self.driver, + unknown.join(", ") + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolution_ok() -> Result<()> { + let nz100 = NonZeroU32::new(100).unwrap(); + let nz200 = NonZeroU32::new(200).unwrap(); + for (s, exp_resolution) in [ + ("100x200", Resolution::Windowed((nz100, nz200))), + ("100x200fs", Resolution::FullScreen((nz100, nz200))), + ("fs", Resolution::FullScreenDesktop), + ] { + assert_eq!(exp_resolution, Resolution::from_str(s)?); + } + Ok(()) + } + + #[test] + fn test_resolution_errors() -> Result<()> { + for (s, exp_error) in [ + ("100", "Invalid resolution 100: must be of the form [WIDTHxHEIGHT][fs]"), + ("100fs", "Invalid resolution 100fs: must be of the form [WIDTHxHEIGHT][fs]"), + ( + "100x200x300", + "Invalid height 200x300 in resolution 100x200x300: invalid digit found in string", + ), + ("100x", "Invalid height in resolution 100x: cannot parse integer from empty string"), + ("x200", "Invalid width in resolution x200: cannot parse integer from empty string"), + ("0x2", "Invalid width 0 in resolution 0x2: number would be zero for non-zero type"), + ("1x0", "Invalid height 0 in resolution 1x0: number would be zero for non-zero type"), + ] { + match Resolution::from_str(s) { + Ok(_) => panic!("Invalid resolution {} not raised as an error", s), + Err(e) => assert_eq!(exp_error, e.0), + } + } + Ok(()) + } + + #[test] + fn test_console_spec_just_driver() -> Result<()> { + let spec = ConsoleSpec::init("default"); + assert_eq!("default", spec.driver); + spec.finish() + } + + #[test] + fn test_console_spec_driver_no_opts() -> Result<()> { + let spec = ConsoleSpec::init("default:"); + assert_eq!("default", spec.driver); + spec.finish() + } + + #[test] + fn test_console_spec_flags() -> Result<()> { + let mut spec = ConsoleSpec::init("default:foo,baz"); + assert_eq!("default", spec.driver); + assert!(spec.take_flag("foo")); + assert!(!spec.take_flag("bar")); + assert!(spec.take_flag("baz")); + spec.finish() + } + + #[test] + fn test_console_spec_keyed_flags() -> Result<()> { + let mut spec = ConsoleSpec::init("default:a=b=c,foo=bar"); + assert_eq!("default", spec.driver); + assert_eq!(Some("b=c"), spec.take_keyed_flag_str("a")); + assert_eq!(Some("bar"), spec.take_keyed_flag_str("foo")); + assert_eq!(None, spec.take_keyed_flag_str("baz")); + spec.finish() + } + + #[test] + fn test_console_spec_keyed_flags_last_wins() -> Result<()> { + let mut spec = ConsoleSpec::init("default:x=1,y=2,x=3"); + assert_eq!("default", spec.driver); + assert_eq!(Some("3"), spec.take_keyed_flag_str("x")); + assert_eq!(Some("2"), spec.take_keyed_flag_str("y")); + spec.finish() + } + + #[test] + fn test_console_spec_keyed_flags_typed_ok() -> Result<()> { + let mut spec = ConsoleSpec::init("default:x=1"); + assert_eq!("default", spec.driver); + assert_eq!(Some(1_i32), spec.take_keyed_flag("x")?); + assert_eq!(None as Option, spec.take_keyed_flag("x")?); + spec.finish() + } + + #[test] + fn test_console_spec_keyed_flags_typed_err() -> Result<()> { + let mut spec = ConsoleSpec::init("default:x=0"); + assert_eq!("default", spec.driver); + assert_eq!( + "Invalid console flag x: number would be zero for non-zero type", + spec.take_keyed_flag::("x").unwrap_err().0 + ); + spec.finish() + } + + #[test] + fn test_console_spec_residue_errors() -> Result<()> { + let mut spec = ConsoleSpec::init("abc:foo,y,x=z,bar=baz"); + assert!(spec.take_flag("foo")); + assert!(!spec.take_flag("x")); + assert_eq!(Some("baz"), spec.take_keyed_flag_str("bar")); + assert_eq!(None, spec.take_keyed_flag_str("y")); + match spec.finish() { + Ok(()) => panic!("Residual flags not detected"), + Err(e) => { + assert_eq!("Console driver abc does not recognize flags: x, y", e.0); + Ok(()) + } + } + } +} From bf9db92f03084ccfe28c82361424e36be9707b5c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 15 Feb 2025 06:30:53 -0800 Subject: [PATCH 025/110] Introduce an SPI abstraction in std This is the first step in decoupling the st7735s console from the rpi crate, which is Linux-specific, as I want to use that code in another target. --- rpi/README.md | 1 + rpi/src/st7735s.rs | 82 ++++++++++++++++++++++++++++++++++++---------- std/src/lib.rs | 1 + std/src/spi.rs | 39 ++++++++++++++++++++++ 4 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 std/src/spi.rs diff --git a/rpi/README.md b/rpi/README.md index 6dad8871..7b36915f 100644 --- a/rpi/README.md +++ b/rpi/README.md @@ -28,3 +28,4 @@ in the `endbasic-std` crate: * Console support on an ST7735S hat. * GPIO pins support. +* SPI bus support. diff --git a/rpi/src/st7735s.rs b/rpi/src/st7735s.rs index 420ec731..9837a139 100644 --- a/rpi/src/st7735s.rs +++ b/rpi/src/st7735s.rs @@ -32,9 +32,11 @@ use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, }; use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY, RGB565Pixel}; +use endbasic_std::spi::{SpiBus, SpiFactory, SpiMode}; use endbasic_terminal::TerminalConsole; use rppal::gpio::{Gpio, InputPin, Level, OutputPin}; use rppal::spi::{self, Bus, SlaveSelect, Spi}; +use std::io::Write; use std::path::Path; use std::time::Duration; use std::{fs, io}; @@ -76,6 +78,55 @@ fn query_spi_bufsiz(path: Option<&Path>) -> io::Result { } } +/// An implementation of an `SpiBus` using rppal. +struct RppalSpiBus { + spi: Spi, + bufsiz: usize, +} + +/// Factory function to open an `RppalSpiBus`. +fn spi_bus_open(bus: u8, slave: u8, clock_hz: u32, mode: SpiMode) -> io::Result { + let bus = match bus { + 0 => Bus::Spi0, + _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only bus 0 is supported")), + }; + + let slave = match slave { + 0 => SlaveSelect::Ss0, + _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only slave 0 is supported")), + }; + + let mode = match mode { + SpiMode::Mode0 => spi::Mode::Mode0, + SpiMode::Mode1 => spi::Mode::Mode1, + SpiMode::Mode2 => spi::Mode::Mode2, + SpiMode::Mode3 => spi::Mode::Mode3, + }; + + let spi = Spi::new(bus, slave, clock_hz, mode).map_err(spi_error_to_io_error)?; + spi.set_ss_polarity(spi::Polarity::ActiveLow).map_err(spi_error_to_io_error)?; + + let bufsiz = query_spi_bufsiz(None)?; + + Ok(RppalSpiBus { spi, bufsiz }) +} + +impl Write for RppalSpiBus { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.spi.write(buf).map_err(spi_error_to_io_error) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl SpiBus for RppalSpiBus { + fn max_size(&self) -> usize { + self.bufsiz + } +} + /// Input handler for the ST7735S console. /// /// This relies on the usual terminal console in raw mode to gather keyboard input but also adds @@ -136,9 +187,8 @@ impl InputOps for ST7735SInput { } /// LCD handler for the ST7735S console. -struct ST7735SLcd { - spi: Spi, - spi_bufsiz: usize, +struct ST7735SLcd { + spi_bus: B, lcd_rst: OutputPin, lcd_dc: OutputPin, @@ -147,9 +197,9 @@ struct ST7735SLcd { size_pixels: LcdSize, } -impl ST7735SLcd { +impl ST7735SLcd { /// Initializes the LCD. - pub fn new(gpio: &mut Gpio) -> io::Result { + pub fn new(gpio: &mut Gpio, spi_factory: SpiFactory) -> io::Result { let mut lcd_cs = gpio.get(8).map_err(gpio_error_to_io_error)?.into_output(); let lcd_rst = gpio.get(27).map_err(gpio_error_to_io_error)?.into_output(); let lcd_dc = gpio.get(25).map_err(gpio_error_to_io_error)?.into_output(); @@ -158,15 +208,11 @@ impl ST7735SLcd { lcd_cs.write(Level::High); lcd_bl.write(Level::High); - let spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 9000000, spi::Mode::Mode0) - .map_err(spi_error_to_io_error)?; - spi.set_ss_polarity(spi::Polarity::ActiveLow).map_err(spi_error_to_io_error)?; - - let spi_bufsiz = query_spi_bufsiz(None)?; + let spi_bus = spi_factory(0, 0, 9000000, SpiMode::Mode0)?; let size_pixels = LcdSize { width: 128, height: 128 }; - let mut device = Self { spi, spi_bufsiz, lcd_rst, lcd_dc, lcd_bl, size_pixels }; + let mut device = Self { spi_bus, lcd_rst, lcd_dc, lcd_bl, size_pixels }; device.lcd_init()?; @@ -177,10 +223,12 @@ impl ST7735SLcd { /// /// The input data is chunked to respect the maximum write size accepted by the SPI bus. fn lcd_write(&mut self, data: &[u8]) -> io::Result<()> { - for chunk in data.chunks(self.spi_bufsiz) { + // TODO(jmmv): Do we really need to chunk the data ourselves, or can we try to write it + // all to the bus and then expect the write to return partial results? + for chunk in data.chunks(self.spi_bus.max_size()) { let mut i = 0; loop { - let n = self.spi.write(&chunk[i..]).map_err(spi_error_to_io_error)?; + let n = self.spi_bus.write(&chunk[i..])?; if n == 0 { break; } @@ -325,13 +373,13 @@ impl ST7735SLcd { } } -impl Drop for ST7735SLcd { +impl Drop for ST7735SLcd { fn drop(&mut self) { self.lcd_bl.write(Level::Low); } } -impl Lcd for ST7735SLcd { +impl Lcd for ST7735SLcd { type Pixel = RGB565Pixel; fn info(&self) -> (LcdSize, usize) { @@ -363,7 +411,7 @@ pub struct ST7735SConsole { /// The graphical console itself. We wrap it in a struct to prevent leaking all auxiliary types /// outside of this crate. - inner: GraphicsConsole>, + inner: GraphicsConsole, Font8>>, } #[async_trait(?Send)] @@ -469,7 +517,7 @@ impl Console for ST7735SConsole { pub fn new_st7735s_console(signals_tx: Sender) -> io::Result { let mut gpio = Gpio::new().map_err(gpio_error_to_io_error)?; - let lcd = ST7735SLcd::new(&mut gpio)?; + let lcd = ST7735SLcd::new(&mut gpio, spi_bus_open)?; let input = ST7735SInput::new(&mut gpio, signals_tx)?; let lcd = BufferedLcd::new(lcd, Font8::default()); let inner = GraphicsConsole::new(input, lcd)?; diff --git a/std/src/lib.rs b/std/src/lib.rs index 77199f1f..ec0d1085 100644 --- a/std/src/lib.rs +++ b/std/src/lib.rs @@ -37,6 +37,7 @@ pub mod gpio; pub mod help; pub mod numerics; pub mod program; +pub mod spi; pub mod storage; pub mod strings; pub mod testutils; diff --git a/std/src/spi.rs b/std/src/spi.rs new file mode 100644 index 00000000..dd02a1fe --- /dev/null +++ b/std/src/spi.rs @@ -0,0 +1,39 @@ +// EndBASIC +// Copyright 2025 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. + +//! SPI bus abstractions for EndBASIC. + +use std::io::{self, Write}; + +/// Defines the SPI clock polarity and phase. +pub enum SpiMode { + /// CPOL 0, CPHA 0 + Mode0 = 0, + /// CPOL 0, CPHA 1 + Mode1 = 1, + /// CPOL 1, CPHA 0 + Mode2 = 2, + /// CPOL 1, CPHA 1 + Mode3 = 3, +} + +/// Factory function to open an SPI bus. +pub type SpiFactory = fn(bus: u8, slave: u8, clock_hz: u32, mode: SpiMode) -> io::Result; + +/// A trait abstracting access to an SPI bus. +pub trait SpiBus: Write { + /// Returns the maximum transfer size for the bus. + fn max_size(&self) -> usize; +} From 87fc82f4d40328bd887365d5393e43590d9fb356 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 15 Feb 2025 06:30:53 -0800 Subject: [PATCH 026/110] Move the rppal SPI code to its own module --- rpi/src/lib.rs | 1 + rpi/src/spi.rs | 113 +++++++++++++++++++++++++++++++++++++++++++++ rpi/src/st7735s.rs | 113 +-------------------------------------------- 3 files changed, 116 insertions(+), 111 deletions(-) create mode 100644 rpi/src/spi.rs diff --git a/rpi/src/lib.rs b/rpi/src/lib.rs index 82b5b7f5..f90ce5dc 100644 --- a/rpi/src/lib.rs +++ b/rpi/src/lib.rs @@ -23,5 +23,6 @@ mod gpio; pub use gpio::RppalPins; +mod spi; mod st7735s; pub use st7735s::new_st7735s_console; diff --git a/rpi/src/spi.rs b/rpi/src/spi.rs new file mode 100644 index 00000000..7cd7f203 --- /dev/null +++ b/rpi/src/spi.rs @@ -0,0 +1,113 @@ +// EndBASIC +// Copyright 2025 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. + +//! SPI bus implementation using rppal. + +use endbasic_std::spi::{SpiBus, SpiMode}; +use rppal::spi::{self, Bus, SlaveSelect, Spi}; +use std::io::Write; +use std::path::Path; +use std::{fs, io}; + +/// Path to the configuration file containing the maximum SPI transfer size. +const SPIDEV_BUFSIZ_PATH: &str = "/sys/module/spidev/parameters/bufsiz"; + +/// Converts an SPI error to an IO error. +fn spi_error_to_io_error(e: spi::Error) -> io::Error { + match e { + spi::Error::Io(e) => e, + e => io::Error::new(io::ErrorKind::InvalidInput, e.to_string()), + } +} + +/// Queries the maximum SPI transfer size from `path`. If `path` is not provided, defaults to +/// `SPIDEV_BUFSIZ_PATH`. +fn query_spi_bufsiz(path: Option<&Path>) -> io::Result { + let path = path.unwrap_or(Path::new(SPIDEV_BUFSIZ_PATH)); + + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(e) => { + return Err(io::Error::new( + e.kind(), + format!("Failed to read {}: {}", path.display(), e), + )); + } + }; + + let content = content.trim_end(); + + match content.parse::() { + Ok(i) => Ok(i), + Err(e) => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to read {}: invalid content: {}", path.display(), e), + )), + } +} + +/// An implementation of an `SpiBus` using rppal. +pub(crate) struct RppalSpiBus { + spi: Spi, + bufsiz: usize, +} + +/// Factory function to open an `RppalSpiBus`. +pub(crate) fn spi_bus_open( + bus: u8, + slave: u8, + clock_hz: u32, + mode: SpiMode, +) -> io::Result { + let bus = match bus { + 0 => Bus::Spi0, + _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only bus 0 is supported")), + }; + + let slave = match slave { + 0 => SlaveSelect::Ss0, + _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only slave 0 is supported")), + }; + + let mode = match mode { + SpiMode::Mode0 => spi::Mode::Mode0, + SpiMode::Mode1 => spi::Mode::Mode1, + SpiMode::Mode2 => spi::Mode::Mode2, + SpiMode::Mode3 => spi::Mode::Mode3, + }; + + let spi = Spi::new(bus, slave, clock_hz, mode).map_err(spi_error_to_io_error)?; + spi.set_ss_polarity(spi::Polarity::ActiveLow).map_err(spi_error_to_io_error)?; + + let bufsiz = query_spi_bufsiz(None)?; + + Ok(RppalSpiBus { spi, bufsiz }) +} + +impl Write for RppalSpiBus { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.spi.write(buf).map_err(spi_error_to_io_error) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl SpiBus for RppalSpiBus { + fn max_size(&self) -> usize { + self.bufsiz + } +} diff --git a/rpi/src/st7735s.rs b/rpi/src/st7735s.rs index 9837a139..66526eca 100644 --- a/rpi/src/st7735s.rs +++ b/rpi/src/st7735s.rs @@ -24,6 +24,7 @@ //! Console driver for the ST7735S LCD. use crate::gpio::gpio_error_to_io_error; +use crate::spi::{spi_bus_open, RppalSpiBus}; use async_channel::Sender; use async_trait::async_trait; use endbasic_core::exec::Signal; @@ -35,97 +36,8 @@ use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY use endbasic_std::spi::{SpiBus, SpiFactory, SpiMode}; use endbasic_terminal::TerminalConsole; use rppal::gpio::{Gpio, InputPin, Level, OutputPin}; -use rppal::spi::{self, Bus, SlaveSelect, Spi}; -use std::io::Write; -use std::path::Path; +use std::io; use std::time::Duration; -use std::{fs, io}; - -/// Path to the configuration file containing the maximum SPI transfer size. -const SPIDEV_BUFSIZ_PATH: &str = "/sys/module/spidev/parameters/bufsiz"; - -/// Converts an SPI error to an IO error. -fn spi_error_to_io_error(e: spi::Error) -> io::Error { - match e { - spi::Error::Io(e) => e, - e => io::Error::new(io::ErrorKind::InvalidInput, e.to_string()), - } -} - -/// Queries the maximum SPI transfer size from `path`. If `path` is not provided, defaults to -/// `SPIDEV_BUFSIZ_PATH`. -fn query_spi_bufsiz(path: Option<&Path>) -> io::Result { - let path = path.unwrap_or(Path::new(SPIDEV_BUFSIZ_PATH)); - - let content = match fs::read_to_string(path) { - Ok(content) => content, - Err(e) => { - return Err(io::Error::new( - e.kind(), - format!("Failed to read {}: {}", path.display(), e), - )); - } - }; - - let content = content.trim_end(); - - match content.parse::() { - Ok(i) => Ok(i), - Err(e) => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Failed to read {}: invalid content: {}", path.display(), e), - )), - } -} - -/// An implementation of an `SpiBus` using rppal. -struct RppalSpiBus { - spi: Spi, - bufsiz: usize, -} - -/// Factory function to open an `RppalSpiBus`. -fn spi_bus_open(bus: u8, slave: u8, clock_hz: u32, mode: SpiMode) -> io::Result { - let bus = match bus { - 0 => Bus::Spi0, - _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only bus 0 is supported")), - }; - - let slave = match slave { - 0 => SlaveSelect::Ss0, - _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only slave 0 is supported")), - }; - - let mode = match mode { - SpiMode::Mode0 => spi::Mode::Mode0, - SpiMode::Mode1 => spi::Mode::Mode1, - SpiMode::Mode2 => spi::Mode::Mode2, - SpiMode::Mode3 => spi::Mode::Mode3, - }; - - let spi = Spi::new(bus, slave, clock_hz, mode).map_err(spi_error_to_io_error)?; - spi.set_ss_polarity(spi::Polarity::ActiveLow).map_err(spi_error_to_io_error)?; - - let bufsiz = query_spi_bufsiz(None)?; - - Ok(RppalSpiBus { spi, bufsiz }) -} - -impl Write for RppalSpiBus { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.spi.write(buf).map_err(spi_error_to_io_error) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl SpiBus for RppalSpiBus { - fn max_size(&self) -> usize { - self.bufsiz - } -} /// Input handler for the ST7735S console. /// @@ -523,24 +435,3 @@ pub fn new_st7735s_console(signals_tx: Sender) -> io::Result Date: Sat, 15 Feb 2025 07:32:45 -0800 Subject: [PATCH 027/110] Make st7735s use our own GPIO abstraction Instead of coupling st7735s to the rppal gpio module, use our own gpio module from std. The change is intrusive due to the differnet way in which we model gpio and, whether our modeling is better than what rppal does... I'm not sure about. But that's where it's at now. --- cli/src/main.rs | 3 + rpi/src/st7735s.rs | 271 +++++++++++++++++++++++++-------------------- 2 files changed, 153 insertions(+), 121 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 3c1933d7..007e937c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -101,6 +101,9 @@ fn new_machine_builder(console_spec: Option<&str>) -> io::Result endbasic_std::MachineBuilder { + // TODO(jmmv): If st7735s is in use, this basically creates a secondary set of pins. + // Which... should work OK, but then the user can interfere with the display in ways that + // should not be allowed, probably. builder.with_gpio_pins(Rc::from(RefCell::from(endbasic_rpi::RppalPins::default()))) } diff --git a/rpi/src/st7735s.rs b/rpi/src/st7735s.rs index 66526eca..be1f3b37 100644 --- a/rpi/src/st7735s.rs +++ b/rpi/src/st7735s.rs @@ -23,8 +23,8 @@ //! Console driver for the ST7735S LCD. -use crate::gpio::gpio_error_to_io_error; use crate::spi::{spi_bus_open, RppalSpiBus}; +use crate::RppalPins; use async_channel::Sender; use async_trait::async_trait; use endbasic_core::exec::Signal; @@ -33,12 +33,29 @@ use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, }; use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY, RGB565Pixel}; +use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiFactory, SpiMode}; use endbasic_terminal::TerminalConsole; -use rppal::gpio::{Gpio, InputPin, Level, OutputPin}; use std::io; +use std::sync::{Arc, Mutex}; use std::time::Duration; +const INPUT_PINS: &[(Pin, Key)] = &[ + (Pin(6), Key::ArrowUp), + (Pin(19), Key::ArrowDown), + (Pin(5), Key::ArrowLeft), + (Pin(26), Key::ArrowRight), + (Pin(13), Key::NewLine), + (Pin(21), Key::Char('1')), + (Pin(20), Key::Char('2')), + (Pin(16), Key::Char('3')), +]; + +const OUTPUT_PIN_CS: Pin = Pin(8); +const OUTPUT_PIN_RST: Pin = Pin(27); +const OUTPUT_PIN_DC: Pin = Pin(25); +const OUTPUT_PIN_BL: Pin = Pin(24); + /// Input handler for the ST7735S console. /// /// This relies on the usual terminal console in raw mode to gather keyboard input but also adds @@ -48,36 +65,41 @@ struct ST7735SInput { } impl ST7735SInput { - fn new(gpio: &mut Gpio, signals_tx: Sender) -> io::Result { + fn new( + pins: Arc>, + signals_tx: Sender, + ) -> io::Result { let (terminal, on_key_tx) = TerminalConsole::from_stdio_with_injector(signals_tx)?; - let key_up = gpio.get(6).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_down = gpio.get(19).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_left = gpio.get(5).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_right = gpio.get(26).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_press = gpio.get(13).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_1 = gpio.get(21).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_2 = gpio.get(20).map_err(gpio_error_to_io_error)?.into_input_pullup(); - let key_3 = gpio.get(16).map_err(gpio_error_to_io_error)?.into_input_pullup(); + { + let mut pins = pins.lock().unwrap(); + for (pin, _key) in INPUT_PINS { + pins.setup(*pin, PinMode::InPullUp)?; + } + } tokio::task::spawn(async move { - async fn read_button(pin: &InputPin, key: Key, tx: &Sender) { - if pin.read() == Level::Low { - if let Err(e) = tx.send(key).await { - eprintln!("Ignoring button {:?} due to error: {}", key, e); + loop { + let mut keys = vec![]; + { + let mut pins = pins.lock().unwrap(); + for (pin, key) in INPUT_PINS { + match pins.read(*pin) { + Ok(false) => keys.push(*key), + Ok(true) => (), + Err(e) => { + eprintln!("Ignoring button {:?} due to error: {}", key, e); + continue; + } + }; } } - } - loop { - read_button(&key_up, Key::ArrowUp, &on_key_tx).await; - read_button(&key_down, Key::ArrowDown, &on_key_tx).await; - read_button(&key_left, Key::ArrowLeft, &on_key_tx).await; - read_button(&key_right, Key::ArrowRight, &on_key_tx).await; - read_button(&key_press, Key::NewLine, &on_key_tx).await; - read_button(&key_1, Key::Char('1'), &on_key_tx).await; - read_button(&key_2, Key::Char('2'), &on_key_tx).await; - read_button(&key_3, Key::Char('3'), &on_key_tx).await; + for key in keys { + if let Err(e) = on_key_tx.send(key).await { + eprintln!("Ignoring button {:?} due to error: {}", key, e); + } + } tokio::time::sleep(Duration::from_millis(50)).await; } @@ -99,32 +121,27 @@ impl InputOps for ST7735SInput { } /// LCD handler for the ST7735S console. -struct ST7735SLcd { +struct ST7735SLcd { + pins: Arc>, spi_bus: B, - - lcd_rst: OutputPin, - lcd_dc: OutputPin, - lcd_bl: OutputPin, - size_pixels: LcdSize, } -impl ST7735SLcd { +impl ST7735SLcd { /// Initializes the LCD. - pub fn new(gpio: &mut Gpio, spi_factory: SpiFactory) -> io::Result { - let mut lcd_cs = gpio.get(8).map_err(gpio_error_to_io_error)?.into_output(); - let lcd_rst = gpio.get(27).map_err(gpio_error_to_io_error)?.into_output(); - let lcd_dc = gpio.get(25).map_err(gpio_error_to_io_error)?.into_output(); - let mut lcd_bl = gpio.get(24).map_err(gpio_error_to_io_error)?.into_output(); - - lcd_cs.write(Level::High); - lcd_bl.write(Level::High); + pub fn new(pins: Arc>, spi_factory: SpiFactory) -> io::Result { + { + let mut pins = pins.lock().unwrap(); + for pin in [OUTPUT_PIN_CS, OUTPUT_PIN_RST, OUTPUT_PIN_DC, OUTPUT_PIN_BL] { + pins.setup(pin, PinMode::Out)?; + } + } let spi_bus = spi_factory(0, 0, 9000000, SpiMode::Mode0)?; let size_pixels = LcdSize { width: 128, height: 128 }; - let mut device = Self { spi_bus, lcd_rst, lcd_dc, lcd_bl, size_pixels }; + let mut device = Self { pins, spi_bus, size_pixels }; device.lcd_init()?; @@ -134,13 +151,13 @@ impl ST7735SLcd { /// Writes arbitrary data to the SPI bus. /// /// The input data is chunked to respect the maximum write size accepted by the SPI bus. - fn lcd_write(&mut self, data: &[u8]) -> io::Result<()> { + fn lcd_write(spi_bus: &mut B, data: &[u8]) -> io::Result<()> { // TODO(jmmv): Do we really need to chunk the data ourselves, or can we try to write it // all to the bus and then expect the write to return partial results? - for chunk in data.chunks(self.spi_bus.max_size()) { + for chunk in data.chunks(spi_bus.max_size()) { let mut i = 0; loop { - let n = self.spi_bus.write(&chunk[i..])?; + let n = spi_bus.write(&chunk[i..])?; if n == 0 { break; } @@ -151,120 +168,132 @@ impl ST7735SLcd { } /// Selects the registers to affect by the next data write. - fn lcd_write_reg(&mut self, regs: &[u8]) -> io::Result<()> { - self.lcd_dc.write(Level::Low); - self.lcd_write(regs) + fn lcd_write_reg(pins: &mut P, spi_bus: &mut B, regs: &[u8]) -> io::Result<()> { + pins.write(OUTPUT_PIN_DC, false)?; + Self::lcd_write(spi_bus, regs) } /// Writes data to the device. A register should have been selected before. - fn lcd_write_data(&mut self, data: &[u8]) -> io::Result<()> { - self.lcd_dc.write(Level::High); - self.lcd_write(data) + fn lcd_write_data(pins: &mut P, spi_bus: &mut B, data: &[u8]) -> io::Result<()> { + pins.write(OUTPUT_PIN_DC, true)?; + Self::lcd_write(spi_bus, data) } /// Resets the LCD. - fn lcd_reset(&mut self) { - self.lcd_rst.write(Level::High); + fn lcd_reset(pins: &mut P) -> io::Result<()> { + pins.write(OUTPUT_PIN_RST, true)?; std::thread::sleep(Duration::from_millis(100)); - self.lcd_rst.write(Level::Low); + pins.write(OUTPUT_PIN_RST, false)?; std::thread::sleep(Duration::from_millis(100)); - self.lcd_rst.write(Level::High); + pins.write(OUTPUT_PIN_RST, true)?; std::thread::sleep(Duration::from_millis(100)); + Ok(()) } /// Sets up the LCD registers. - fn lcd_init_reg(&mut self) -> io::Result<()> { + fn lcd_init_reg(pins: &mut P, spi_bus: &mut B) -> io::Result<()> { // ST7735R Frame Rate. - self.lcd_write_reg(&[0xb1])?; - self.lcd_write_data(&[0x01, 0x2c, 0x2d])?; + Self::lcd_write_reg(pins, spi_bus, &[0xb1])?; + Self::lcd_write_data(pins, spi_bus, &[0x01, 0x2c, 0x2d])?; - self.lcd_write_reg(&[0xb2])?; - self.lcd_write_data(&[0x01, 0x2c, 0x2d])?; + Self::lcd_write_reg(pins, spi_bus, &[0xb2])?; + Self::lcd_write_data(pins, spi_bus, &[0x01, 0x2c, 0x2d])?; - self.lcd_write_reg(&[0xb3])?; - self.lcd_write_data(&[0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])?; + Self::lcd_write_reg(pins, spi_bus, &[0xb3])?; + Self::lcd_write_data(pins, spi_bus, &[0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])?; // Column inversion. - self.lcd_write_reg(&[0xb4])?; - self.lcd_write_data(&[0x07])?; + Self::lcd_write_reg(pins, spi_bus, &[0xb4])?; + Self::lcd_write_data(pins, spi_bus, &[0x07])?; // ST7735R Power Sequence. - self.lcd_write_reg(&[0xc0])?; - self.lcd_write_data(&[0xa2, 0x02, 0x84])?; - self.lcd_write_reg(&[0xc1])?; - self.lcd_write_data(&[0xc5])?; + Self::lcd_write_reg(pins, spi_bus, &[0xc0])?; + Self::lcd_write_data(pins, spi_bus, &[0xa2, 0x02, 0x84])?; + Self::lcd_write_reg(pins, spi_bus, &[0xc1])?; + Self::lcd_write_data(pins, spi_bus, &[0xc5])?; - self.lcd_write_reg(&[0xc2])?; - self.lcd_write_data(&[0x0a, 0x00])?; + Self::lcd_write_reg(pins, spi_bus, &[0xc2])?; + Self::lcd_write_data(pins, spi_bus, &[0x0a, 0x00])?; - self.lcd_write_reg(&[0xc3])?; - self.lcd_write_data(&[0x8a, 0x2a])?; - self.lcd_write_reg(&[0xc4])?; - self.lcd_write_data(&[0x8a, 0xee])?; + Self::lcd_write_reg(pins, spi_bus, &[0xc3])?; + Self::lcd_write_data(pins, spi_bus, &[0x8a, 0x2a])?; + Self::lcd_write_reg(pins, spi_bus, &[0xc4])?; + Self::lcd_write_data(pins, spi_bus, &[0x8a, 0xee])?; // VCOM. - self.lcd_write_reg(&[0xc5])?; - self.lcd_write_data(&[0x0e])?; + Self::lcd_write_reg(pins, spi_bus, &[0xc5])?; + Self::lcd_write_data(pins, spi_bus, &[0x0e])?; // ST7735R Gamma Sequence. - self.lcd_write_reg(&[0xe0])?; - self.lcd_write_data(&[ - 0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f, 0x1b, 0x23, 0x37, 0x00, 0x07, - 0x02, 0x10, - ])?; - - self.lcd_write_reg(&[0xe1])?; - self.lcd_write_data(&[ - 0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30, 0x30, 0x39, 0x3f, 0x00, 0x07, - 0x03, 0x10, - ])?; + Self::lcd_write_reg(pins, spi_bus, &[0xe0])?; + Self::lcd_write_data( + pins, + spi_bus, + &[ + 0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f, 0x1b, 0x23, 0x37, 0x00, 0x07, + 0x02, 0x10, + ], + )?; + + Self::lcd_write_reg(pins, spi_bus, &[0xe1])?; + Self::lcd_write_data( + pins, + spi_bus, + &[ + 0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30, 0x30, 0x39, 0x3f, 0x00, 0x07, + 0x03, 0x10, + ], + )?; // Enable test command. - self.lcd_write_reg(&[0xf0])?; - self.lcd_write_data(&[0x01])?; + Self::lcd_write_reg(pins, spi_bus, &[0xf0])?; + Self::lcd_write_data(pins, spi_bus, &[0x01])?; // Disable ram power save mode. - self.lcd_write_reg(&[0xf6])?; - self.lcd_write_data(&[0x00])?; + Self::lcd_write_reg(pins, spi_bus, &[0xf6])?; + Self::lcd_write_data(pins, spi_bus, &[0x00])?; // 65k mode. - self.lcd_write_reg(&[0x3a])?; - self.lcd_write_data(&[0x05])?; + Self::lcd_write_reg(pins, spi_bus, &[0x3a])?; + Self::lcd_write_data(pins, spi_bus, &[0x05])?; Ok(()) } /// Initializes the LCD scan direction and pixel color encoding. - fn lcd_set_gram_scan_way(&mut self) -> io::Result<()> { - self.lcd_write_reg(&[0x36])?; // MX, MY, RGB mode. + fn lcd_set_gram_scan_way(pins: &mut P, spi_bus: &mut B) -> io::Result<()> { + Self::lcd_write_reg(pins, spi_bus, &[0x36])?; // MX, MY, RGB mode. let scan_dir = 0x40 | 0x20; // X, Y. let rgb_mode = 0x08; // RGB for 1.44in display. - self.lcd_write_data(&[scan_dir | rgb_mode])?; + Self::lcd_write_data(pins, spi_bus, &[scan_dir | rgb_mode])?; Ok(()) } /// Initializes the LCD. fn lcd_init(&mut self) -> io::Result<()> { - self.lcd_bl.write(Level::High); + let mut pins = self.pins.lock().unwrap(); - self.lcd_reset(); - self.lcd_init_reg()?; + pins.write(OUTPUT_PIN_CS, true)?; + pins.write(OUTPUT_PIN_BL, true)?; - self.lcd_set_gram_scan_way()?; + Self::lcd_reset(&mut *pins)?; + Self::lcd_init_reg(&mut *pins, &mut self.spi_bus)?; + + Self::lcd_set_gram_scan_way(&mut *pins, &mut self.spi_bus)?; std::thread::sleep(Duration::from_millis(200)); - self.lcd_write_reg(&[0x11])?; + Self::lcd_write_reg(&mut *pins, &mut self.spi_bus, &[0x11])?; std::thread::sleep(Duration::from_millis(200)); // Turn display on. - self.lcd_write_reg(&[0x29])?; + Self::lcd_write_reg(&mut *pins, &mut self.spi_bus, &[0x29])?; Ok(()) } /// Configures the LCD so that the next write, which carries pixel data, affects the specified /// region. - fn lcd_set_window(&mut self, xy: LcdXY, size: LcdSize) -> io::Result<()> { + fn lcd_set_window(pins: &mut P, spi_bus: &mut B, xy: LcdXY, size: LcdSize) -> io::Result<()> { let adjust_x = 1; let adjust_y = 2; @@ -273,25 +302,26 @@ impl ST7735SLcd { let y1 = ((xy.y & 0xff) + adjust_y) as u8; let y2 = (((xy.y + size.height) + adjust_y - 1) & 0xff) as u8; - self.lcd_write_reg(&[0x2a])?; - self.lcd_write_data(&[0x00, x1, 0x00, x2])?; + Self::lcd_write_reg(pins, spi_bus, &[0x2a])?; + Self::lcd_write_data(pins, spi_bus, &[0x00, x1, 0x00, x2])?; - self.lcd_write_reg(&[0x2b])?; - self.lcd_write_data(&[0x00, y1, 0x00, y2])?; + Self::lcd_write_reg(pins, spi_bus, &[0x2b])?; + Self::lcd_write_data(pins, spi_bus, &[0x00, y1, 0x00, y2])?; - self.lcd_write_reg(&[0x2c])?; + Self::lcd_write_reg(pins, spi_bus, &[0x2c])?; Ok(()) } } -impl Drop for ST7735SLcd { +impl Drop for ST7735SLcd { fn drop(&mut self) { - self.lcd_bl.write(Level::Low); + let mut pins = self.pins.lock().unwrap(); + let _result = pins.write(OUTPUT_PIN_BL, false); } } -impl Lcd for ST7735SLcd { +impl Lcd for ST7735SLcd { type Pixel = RGB565Pixel; fn info(&self) -> (LcdSize, usize) { @@ -310,20 +340,19 @@ impl Lcd for ST7735SLcd { fn set_data(&mut self, x1y1: LcdXY, x2y2: LcdXY, data: &[u8]) -> io::Result<()> { let (xy, size) = to_xy_size(x1y1, x2y2); - self.lcd_set_window(xy, size)?; - self.lcd_write_data(data) + let mut pins = self.pins.lock().unwrap(); + Self::lcd_set_window(&mut *pins, &mut self.spi_bus, xy, size)?; + Self::lcd_write_data(&mut *pins, &mut self.spi_bus, data) } } /// Console implementation using a ST7735S LCD. +// TODO(jmmv): Delete this wrapper. Once the console moves to `std`, I don't think there will be +// anything left behind to "hide". pub struct ST7735SConsole { - /// GPIO controller used for the LCD and the input buttons. Must be kept alive for as long as - /// `inner` is. - _gpio: Gpio, - /// The graphical console itself. We wrap it in a struct to prevent leaking all auxiliary types /// outside of this crate. - inner: GraphicsConsole, Font8>>, + inner: GraphicsConsole, Font8>>, } #[async_trait(?Send)] @@ -427,11 +456,11 @@ impl Console for ST7735SConsole { /// Initializes a new console on a ST7735S LCD. pub fn new_st7735s_console(signals_tx: Sender) -> io::Result { - let mut gpio = Gpio::new().map_err(gpio_error_to_io_error)?; + let pins = Arc::from(Mutex::from(RppalPins::default())); - let lcd = ST7735SLcd::new(&mut gpio, spi_bus_open)?; - let input = ST7735SInput::new(&mut gpio, signals_tx)?; + let lcd = ST7735SLcd::new(pins.clone(), spi_bus_open)?; + let input = ST7735SInput::new(pins, signals_tx)?; let lcd = BufferedLcd::new(lcd, Font8::default()); let inner = GraphicsConsole::new(input, lcd)?; - Ok(ST7735SConsole { _gpio: gpio, inner }) + Ok(ST7735SConsole { inner }) } From 6109c2099ab09c13067555323ff81a6bb873809f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 15 Feb 2025 08:30:12 -0800 Subject: [PATCH 028/110] Move the st7735s console to its own crate --- Cargo.toml | 1 + NEWS.md | 4 + RELEASE.md | 1 + cli/Cargo.toml | 7 +- cli/src/main.rs | 7 +- rpi/Cargo.toml | 14 -- rpi/README.md | 1 - rpi/src/lib.rs | 3 +- rpi/src/spi.rs | 9 +- st7735s/Cargo.toml | 32 ++++ st7735s/LICENSE | 202 +++++++++++++++++++++++ st7735s/NOTICE | 35 ++++ st7735s/README.md | 26 +++ rpi/src/st7735s.rs => st7735s/src/lib.rs | 23 ++- 14 files changed, 327 insertions(+), 38 deletions(-) create mode 100644 st7735s/Cargo.toml create mode 100644 st7735s/LICENSE create mode 100644 st7735s/NOTICE create mode 100644 st7735s/README.md rename rpi/src/st7735s.rs => st7735s/src/lib.rs (96%) diff --git a/Cargo.toml b/Cargo.toml index 35caf422..12c29593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "repl", "rpi", "sdl", + "st7735s", "std", "terminal", "web", diff --git a/NEWS.md b/NEWS.md index 4a175de1..9f8e8951 100644 --- a/NEWS.md +++ b/NEWS.md @@ -24,6 +24,10 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. `--console=graphics`) and the optional parameters given to `sdl` are now of the form `key=value` parameters. +* Added a new `endbasic-st7735s` crate to assimilate the driver for the + ST7735S LCD, decoupling its implementation from the Linux-specific bits in + the `endbasic-rpi` crate. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/RELEASE.md b/RELEASE.md index 894bbc8f..88c19ce2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -39,5 +39,6 @@ * `( cd client && cargo publish )` * `( cd terminal && cargo publish )` * `( cd sdl && cargo publish )` + * `( cd st7735s && cargo publish )` * `( cd rpi && cargo publish )` * `( cd cli && cargo publish )` diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e2cdc022..6217e9b9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -15,7 +15,7 @@ edition = "2018" default = ["crossterm"] crossterm = ["endbasic-terminal"] sdl = ["endbasic-sdl"] -rpi = ["endbasic-rpi"] +rpi = ["endbasic-rpi", "endbasic-st7735s"] [dependencies] anyhow = "1.0" @@ -46,6 +46,11 @@ version = "0.11.99" # ENDBASIC-VERSION path = "../sdl" optional = true +[dependencies.endbasic-st7735s] +version = "0.11.99" # ENDBASIC-VERSION +path = "../st7735s" +optional = true + [dependencies.endbasic-std] version = "0.11.99" # ENDBASIC-VERSION path = "../std" diff --git a/cli/src/main.rs b/cli/src/main.rs index 007e937c..d14736e8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -212,7 +212,12 @@ fn setup_console( #[cfg(feature = "rpi")] fn setup_st7735s_console(signals_tx: Sender) -> io::Result>> { - Ok(Rc::from(RefCell::from(endbasic_rpi::new_st7735s_console(signals_tx)?))) + let console = endbasic_st7735s::new_console( + endbasic_rpi::RppalPins::default(), + endbasic_rpi::spi_bus_open, + signals_tx, + )?; + Ok(Rc::from(RefCell::from(console))) } #[cfg(not(feature = "rpi"))] diff --git a/rpi/Cargo.toml b/rpi/Cargo.toml index 6c70a6d1..18c4d6d5 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -12,22 +12,8 @@ readme = "README.md" edition = "2018" [dependencies] -async-channel = "2.2" -async-trait = "0.1" rppal = "0.17" -tokio = { version = "1", features = ["full"] } - -[dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION -path = "../core" [dependencies.endbasic-std] version = "0.11.99" # ENDBASIC-VERSION path = "../std" - -[dependencies.endbasic-terminal] -version = "0.11.99" # ENDBASIC-VERSION -path = "../terminal" - -[dev-dependencies] -tempfile = "3" diff --git a/rpi/README.md b/rpi/README.md index 7b36915f..60f4da57 100644 --- a/rpi/README.md +++ b/rpi/README.md @@ -26,6 +26,5 @@ EndBASIC is free software under the [Apache 2.0 License](LICENSE). `endbasic-rpi` provides Raspberry Pi support for the following features defined in the `endbasic-std` crate: -* Console support on an ST7735S hat. * GPIO pins support. * SPI bus support. diff --git a/rpi/src/lib.rs b/rpi/src/lib.rs index f90ce5dc..c56f56e7 100644 --- a/rpi/src/lib.rs +++ b/rpi/src/lib.rs @@ -24,5 +24,4 @@ mod gpio; pub use gpio::RppalPins; mod spi; -mod st7735s; -pub use st7735s::new_st7735s_console; +pub use spi::{spi_bus_open, RppalSpiBus}; diff --git a/rpi/src/spi.rs b/rpi/src/spi.rs index 7cd7f203..70afa93d 100644 --- a/rpi/src/spi.rs +++ b/rpi/src/spi.rs @@ -59,18 +59,13 @@ fn query_spi_bufsiz(path: Option<&Path>) -> io::Result { } /// An implementation of an `SpiBus` using rppal. -pub(crate) struct RppalSpiBus { +pub struct RppalSpiBus { spi: Spi, bufsiz: usize, } /// Factory function to open an `RppalSpiBus`. -pub(crate) fn spi_bus_open( - bus: u8, - slave: u8, - clock_hz: u32, - mode: SpiMode, -) -> io::Result { +pub fn spi_bus_open(bus: u8, slave: u8, clock_hz: u32, mode: SpiMode) -> io::Result { let bus = match bus { 0 => Bus::Spi0, _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Only bus 0 is supported")), diff --git a/st7735s/Cargo.toml b/st7735s/Cargo.toml new file mode 100644 index 00000000..fb306d51 --- /dev/null +++ b/st7735s/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "endbasic-st7735s" +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 - st7735s console" +homepage = "https://www.endbasic.dev/" +repository = "https://github.com/endbasic/endbasic" +readme = "README.md" +edition = "2018" + +[dependencies] +async-channel = "2.2" +async-trait = "0.1" +tokio = { version = "1", features = ["full"] } + +[dependencies.endbasic-core] +version = "0.11.99" # ENDBASIC-VERSION +path = "../core" + +[dependencies.endbasic-std] +version = "0.11.99" # ENDBASIC-VERSION +path = "../std" + +[dependencies.endbasic-terminal] +version = "0.11.99" # ENDBASIC-VERSION +path = "../terminal" + +[dev-dependencies] +tempfile = "3" diff --git a/st7735s/LICENSE b/st7735s/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/st7735s/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/st7735s/NOTICE b/st7735s/NOTICE new file mode 100644 index 00000000..2fd5c019 --- /dev/null +++ b/st7735s/NOTICE @@ -0,0 +1,35 @@ +EndBASIC +Copyright 2020-2025 Julio Merino + +---- + +The code and the font to support the ST7735S LCD are based on the +materials provided by the device's sample code, which carry the following +license: + +Copyright (c) 2014 STMicroelectronics + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of STMicroelectronics nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/st7735s/README.md b/st7735s/README.md new file mode 100644 index 00000000..89b93881 --- /dev/null +++ b/st7735s/README.md @@ -0,0 +1,26 @@ +# The EndBASIC programming language - st7735s console + +[![Crates.io](https://img.shields.io/crates/v/endbasic-st7735s.svg)](https://crates.io/crates/endbasic-st7735s/) +[![Docs.rs](https://docs.rs/endbasic-st7735s/badge.svg)](https://docs.rs/endbasic-st7735s/) + +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-st7735s` provides a console implementation on an ST7735S hat. diff --git a/rpi/src/st7735s.rs b/st7735s/src/lib.rs similarity index 96% rename from rpi/src/st7735s.rs rename to st7735s/src/lib.rs index be1f3b37..5076cf09 100644 --- a/rpi/src/st7735s.rs +++ b/st7735s/src/lib.rs @@ -23,8 +23,6 @@ //! Console driver for the ST7735S LCD. -use crate::spi::{spi_bus_open, RppalSpiBus}; -use crate::RppalPins; use async_channel::Sender; use async_trait::async_trait; use endbasic_core::exec::Signal; @@ -346,17 +344,15 @@ impl Lcd for ST7735SLcd { } } -/// Console implementation using a ST7735S LCD. -// TODO(jmmv): Delete this wrapper. Once the console moves to `std`, I don't think there will be -// anything left behind to "hide". -pub struct ST7735SConsole { +/// Console implementation using an ST7735S LCD. +pub struct ST7735SConsole { /// The graphical console itself. We wrap it in a struct to prevent leaking all auxiliary types /// outside of this crate. - inner: GraphicsConsole, Font8>>, + inner: GraphicsConsole, Font8>>, } #[async_trait(?Send)] -impl Console for ST7735SConsole { +impl Console for ST7735SConsole { fn clear(&mut self, how: ClearType) -> io::Result<()> { self.inner.clear(how) } @@ -455,10 +451,13 @@ impl Console for ST7735SConsole { } /// Initializes a new console on a ST7735S LCD. -pub fn new_st7735s_console(signals_tx: Sender) -> io::Result { - let pins = Arc::from(Mutex::from(RppalPins::default())); - - let lcd = ST7735SLcd::new(pins.clone(), spi_bus_open)?; +pub fn new_console( + pins: P, + new_spi: SpiFactory, + signals_tx: Sender, +) -> io::Result> { + let pins = Arc::from(Mutex::from(pins)); + let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; let input = ST7735SInput::new(pins, signals_tx)?; let lcd = BufferedLcd::new(lcd, Font8::default()); let inner = GraphicsConsole::new(input, lcd)?; From 4ae09932cd948ad492cab213424c36a2e2f85250 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Wed, 19 Feb 2025 20:57:05 -0800 Subject: [PATCH 029/110] Decouple st7735s from terminal Instead of explicitly relying on TerminalConsole from within the ST7735S implementation, just request a "backing" keyboard and multiplex it with the buttons of the LCD. --- cli/src/main.rs | 2 +- st7735s/Cargo.toml | 4 --- st7735s/src/lib.rs | 60 ++++++++++++++++++++++++++------------------- terminal/src/lib.rs | 30 ++++++++++++++++------- 4 files changed, 57 insertions(+), 39 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index d14736e8..22094bdd 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -215,7 +215,7 @@ fn setup_console( let console = endbasic_st7735s::new_console( endbasic_rpi::RppalPins::default(), endbasic_rpi::spi_bus_open, - signals_tx, + endbasic_terminal::TerminalConsole::from_stdio(signals_tx)?, )?; Ok(Rc::from(RefCell::from(console))) } diff --git a/st7735s/Cargo.toml b/st7735s/Cargo.toml index fb306d51..9e8c7ae5 100644 --- a/st7735s/Cargo.toml +++ b/st7735s/Cargo.toml @@ -24,9 +24,5 @@ path = "../core" version = "0.11.99" # ENDBASIC-VERSION path = "../std" -[dependencies.endbasic-terminal] -version = "0.11.99" # ENDBASIC-VERSION -path = "../terminal" - [dev-dependencies] tempfile = "3" diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index 5076cf09..c2e9f031 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -23,9 +23,8 @@ //! Console driver for the ST7735S LCD. -use async_channel::Sender; +use async_channel::{Receiver, TryRecvError}; use async_trait::async_trait; -use endbasic_core::exec::Signal; use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, @@ -33,7 +32,6 @@ use endbasic_std::console::{ use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY, RGB565Pixel}; use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiFactory, SpiMode}; -use endbasic_terminal::TerminalConsole; use std::io; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -56,18 +54,18 @@ const OUTPUT_PIN_BL: Pin = Pin(24); /// Input handler for the ST7735S console. /// -/// This relies on the usual terminal console in raw mode to gather keyboard input but also adds -/// support for the physical buttons that come along with the display. -struct ST7735SInput { - terminal: TerminalConsole, +/// This driver reads the (limited) physical buttons of the ST7735S device and multiplexes them with +/// a real keyboard. +struct ST7735SInput { + on_button_rx: Receiver, + keyboard: K, } -impl ST7735SInput { - fn new( - pins: Arc>, - signals_tx: Sender, - ) -> io::Result { - let (terminal, on_key_tx) = TerminalConsole::from_stdio_with_injector(signals_tx)?; +impl ST7735SInput { + /// Constructs a new input handler that reads button presses through `pins` and multiplexes them + /// with `keyboard`. + fn new(pins: Arc>, keyboard: K) -> io::Result { + let (on_button_tx, on_button_rx) = async_channel::unbounded(); { let mut pins = pins.lock().unwrap(); @@ -94,7 +92,7 @@ impl ST7735SInput { } for key in keys { - if let Err(e) = on_key_tx.send(key).await { + if let Err(e) = on_button_tx.send(key).await { eprintln!("Ignoring button {:?} due to error: {}", key, e); } } @@ -103,18 +101,30 @@ impl ST7735SInput { } }); - Ok(Self { terminal }) + Ok(Self { on_button_rx, keyboard }) } } #[async_trait(?Send)] -impl InputOps for ST7735SInput { +impl InputOps for ST7735SInput { async fn poll_key(&mut self) -> io::Result> { - self.terminal.poll_key().await + match self.on_button_rx.try_recv() { + Ok(k) => Ok(Some(k)), + Err(TryRecvError::Empty) => self.keyboard.poll_key().await, + Err(TryRecvError::Closed) => Ok(Some(Key::Eof)), + } } async fn read_key(&mut self) -> io::Result { - self.terminal.read_key().await + tokio::select! { + result = self.on_button_rx.recv() => { + match result { + Ok(k) => Ok(k), + Err(_) => Ok(Key::Eof), + } + } + result = self.keyboard.read_key() => result, + } } } @@ -345,14 +355,14 @@ impl Lcd for ST7735SLcd { } /// Console implementation using an ST7735S LCD. -pub struct ST7735SConsole { +pub struct ST7735SConsole { /// The graphical console itself. We wrap it in a struct to prevent leaking all auxiliary types /// outside of this crate. - inner: GraphicsConsole, Font8>>, + inner: GraphicsConsole, BufferedLcd, Font8>>, } #[async_trait(?Send)] -impl Console for ST7735SConsole { +impl Console for ST7735SConsole { fn clear(&mut self, how: ClearType) -> io::Result<()> { self.inner.clear(how) } @@ -451,14 +461,14 @@ impl Console for ST7735SConsole { } /// Initializes a new console on a ST7735S LCD. -pub fn new_console( +pub fn new_console( pins: P, new_spi: SpiFactory, - signals_tx: Sender, -) -> io::Result> { + keyboard: K, +) -> io::Result> { let pins = Arc::from(Mutex::from(pins)); let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; - let input = ST7735SInput::new(pins, signals_tx)?; + let input = ST7735SInput::new(pins, keyboard)?; let lcd = BufferedLcd::new(lcd, Font8::default()); let inner = GraphicsConsole::new(input, lcd)?; Ok(ST7735SConsole { inner }) diff --git a/terminal/src/lib.rs b/terminal/src/lib.rs index 1b1db836..2bafa075 100644 --- a/terminal/src/lib.rs +++ b/terminal/src/lib.rs @@ -28,6 +28,7 @@ use crossterm::event::{self, KeyEventKind}; use crossterm::tty::IsTty; use crossterm::{cursor, style, terminal, QueueableCommand}; 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, }; @@ -234,6 +235,24 @@ impl TerminalConsole { } } +#[async_trait(?Send)] +impl InputOps for TerminalConsole { + async fn poll_key(&mut self) -> io::Result> { + match self.on_key_rx.try_recv() { + Ok(k) => Ok(Some(k)), + Err(TryRecvError::Empty) => Ok(None), + Err(TryRecvError::Closed) => Ok(Some(Key::Eof)), + } + } + + async fn read_key(&mut self) -> io::Result { + match self.on_key_rx.recv().await { + Ok(k) => Ok(k), + Err(_) => Ok(Key::Eof), + } + } +} + #[async_trait(?Send)] impl Console for TerminalConsole { fn clear(&mut self, how: ClearType) -> io::Result<()> { @@ -367,18 +386,11 @@ impl Console for TerminalConsole { } async fn poll_key(&mut self) -> io::Result> { - match self.on_key_rx.try_recv() { - Ok(k) => Ok(Some(k)), - Err(TryRecvError::Empty) => Ok(None), - Err(TryRecvError::Closed) => Ok(Some(Key::Eof)), - } + (self as &mut dyn InputOps).poll_key().await } async fn read_key(&mut self) -> io::Result { - match self.on_key_rx.recv().await { - Ok(k) => Ok(k), - Err(_) => Ok(Key::Eof), - } + (self as &mut dyn InputOps).read_key().await } fn show_cursor(&mut self) -> io::Result<()> { From 92736292d4bc7f69768a6f41ef4f0fa3f69be31a Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 17 Feb 2025 06:50:09 -0800 Subject: [PATCH 030/110] Make the ST7735S LCD work on NetBSD I do not know what exactly the CS pin controls but setting it to high, as we currently do, causes the LCD to remain lit up on NetBSD. On Linux though, the value of this pin seems to make no difference, which is very strange. But, for now, let's just keep it low. While here, also remove the explicit call to set the SPI slave select polarity to low (because I investigates this as part of the differences indicated above). This is the default anyway based on what I read in the code. --- rpi/src/spi.rs | 1 - st7735s/src/lib.rs | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rpi/src/spi.rs b/rpi/src/spi.rs index 70afa93d..da5d3a57 100644 --- a/rpi/src/spi.rs +++ b/rpi/src/spi.rs @@ -84,7 +84,6 @@ pub fn spi_bus_open(bus: u8, slave: u8, clock_hz: u32, mode: SpiMode) -> io::Res }; let spi = Spi::new(bus, slave, clock_hz, mode).map_err(spi_error_to_io_error)?; - spi.set_ss_polarity(spi::Polarity::ActiveLow).map_err(spi_error_to_io_error)?; let bufsiz = query_spi_bufsiz(None)?; diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index c2e9f031..b7fcd2de 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -281,7 +281,10 @@ impl ST7735SLcd { fn lcd_init(&mut self) -> io::Result<()> { let mut pins = self.pins.lock().unwrap(); - pins.write(OUTPUT_PIN_CS, true)?; + // I'm not sure what this does. This does not have an effect on Linux but + // setting this to high on NetBSD causes the LCD to remain lit up. + pins.write(OUTPUT_PIN_CS, false)?; + pins.write(OUTPUT_PIN_BL, true)?; Self::lcd_reset(&mut *pins)?; From b9349dee3324592e4b04b4db1571ce428909bbad Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 20 Feb 2025 19:57:57 -0800 Subject: [PATCH 031/110] Prevent zero-length writes to the SPI bus The NetBSD kernel interface doesn't like it when we try to send an empty buffer to the SPI bus, and it's just unnecessary to go through the loop an extra time. --- st7735s/src/lib.rs | 94 +++++++++++++++++++++++++++++++++++----------- std/src/spi.rs | 1 + 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index b7fcd2de..b31f5ac5 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -128,6 +128,25 @@ impl InputOps for ST7735SInput { } } +/// Writes arbitrary data to the SPI bus. +/// +/// The input data is chunked to respect the maximum write size accepted by the SPI bus. +fn lcd_write(spi_bus: &mut B, data: &[u8]) -> io::Result<()> { + // TODO(jmmv): Do we really need to chunk the data ourselves, or can we try to write it + // all to the bus and then expect the write to return partial results? + for chunk in data.chunks(spi_bus.max_size()) { + let mut i = 0; + loop { + let n = spi_bus.write(&chunk[i..])?; + if n == chunk.len() - i { + break; + } + i += n; + } + } + Ok(()) +} + /// LCD handler for the ST7735S console. struct ST7735SLcd { pins: Arc>, @@ -156,35 +175,16 @@ impl ST7735SLcd { Ok(device) } - /// Writes arbitrary data to the SPI bus. - /// - /// The input data is chunked to respect the maximum write size accepted by the SPI bus. - fn lcd_write(spi_bus: &mut B, data: &[u8]) -> io::Result<()> { - // TODO(jmmv): Do we really need to chunk the data ourselves, or can we try to write it - // all to the bus and then expect the write to return partial results? - for chunk in data.chunks(spi_bus.max_size()) { - let mut i = 0; - loop { - let n = spi_bus.write(&chunk[i..])?; - if n == 0 { - break; - } - i += n; - } - } - Ok(()) - } - /// Selects the registers to affect by the next data write. fn lcd_write_reg(pins: &mut P, spi_bus: &mut B, regs: &[u8]) -> io::Result<()> { pins.write(OUTPUT_PIN_DC, false)?; - Self::lcd_write(spi_bus, regs) + lcd_write(spi_bus, regs) } /// Writes data to the device. A register should have been selected before. fn lcd_write_data(pins: &mut P, spi_bus: &mut B, data: &[u8]) -> io::Result<()> { pins.write(OUTPUT_PIN_DC, true)?; - Self::lcd_write(spi_bus, data) + lcd_write(spi_bus, data) } /// Resets the LCD. @@ -476,3 +476,55 @@ pub fn new_console( let inner = GraphicsConsole::new(input, lcd)?; Ok(ST7735SConsole { inner }) } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[derive(Default)] + struct MockSpiBus { + max_size: usize, + + writes: Vec>, + } + + impl Write for MockSpiBus { + fn write(&mut self, buf: &[u8]) -> io::Result { + let partial = if buf.len() < self.max_size { buf } else { &buf[0..self.max_size] }; + self.writes.push(partial.to_owned()); + Ok(partial.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + impl SpiBus for MockSpiBus { + fn max_size(&self) -> usize { + self.max_size + } + } + + #[test] + fn test_lcd_write_shorter_than_max_size() { + let mut bus = MockSpiBus { max_size: 100, ..Default::default() }; + lcd_write(&mut bus, &[0, 1, 2, 3, 4]).unwrap(); + assert_eq!(vec![vec![0, 1, 2, 3, 4]], bus.writes); + } + + #[test] + fn test_lcd_write_equal_to_max_size() { + let mut bus = MockSpiBus { max_size: 3, ..Default::default() }; + lcd_write(&mut bus, &[0, 1, 2]).unwrap(); + assert_eq!(vec![vec![0, 1, 2]], bus.writes); + } + + #[test] + fn test_lcd_write_greater_than_max_size() { + let mut bus = MockSpiBus { max_size: 6, ..Default::default() }; + lcd_write(&mut bus, &[0, 1, 2, 3, 4, 5, 6]).unwrap(); + assert_eq!(vec![vec![0, 1, 2, 3, 4, 5], vec![6]], bus.writes); + } +} diff --git a/std/src/spi.rs b/std/src/spi.rs index dd02a1fe..31c498b5 100644 --- a/std/src/spi.rs +++ b/std/src/spi.rs @@ -18,6 +18,7 @@ use std::io::{self, Write}; /// Defines the SPI clock polarity and phase. +#[derive(Debug, PartialEq)] pub enum SpiMode { /// CPOL 0, CPHA 0 Mode0 = 0, From d2b566cea6cf79dc72808c58cb80420f0a0f86c7 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 21 Feb 2025 06:21:00 -0800 Subject: [PATCH 032/110] Turn the SpiFactory into an FnOnce This is to support closures. Unfortunately, I can't keep the alias around because aliases for impl closures are Rust nightly only. --- st7735s/src/lib.rs | 19 ++++++++++++++----- std/src/spi.rs | 5 +---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index b31f5ac5..8a4c8e49 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -31,7 +31,7 @@ use endbasic_std::console::{ }; use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY, RGB565Pixel}; use endbasic_std::gpio::{Pin, PinMode, Pins}; -use endbasic_std::spi::{SpiBus, SpiFactory, SpiMode}; +use endbasic_std::spi::{SpiBus, SpiMode}; use std::io; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -156,7 +156,10 @@ struct ST7735SLcd { impl ST7735SLcd { /// Initializes the LCD. - pub fn new(pins: Arc>, spi_factory: SpiFactory) -> io::Result { + pub fn new(pins: Arc>, spi_factory: F) -> io::Result + where + F: FnOnce(u8, u8, u32, SpiMode) -> io::Result, + { { let mut pins = pins.lock().unwrap(); for pin in [OUTPUT_PIN_CS, OUTPUT_PIN_RST, OUTPUT_PIN_DC, OUTPUT_PIN_BL] { @@ -464,11 +467,17 @@ impl Console for ST7735SConsole } /// Initializes a new console on a ST7735S LCD. -pub fn new_console( +pub fn new_console( pins: P, - new_spi: SpiFactory, + new_spi: F, keyboard: K, -) -> io::Result> { +) -> io::Result> +where + P: Pins + Send + 'static, + F: FnOnce(u8, u8, u32, SpiMode) -> io::Result, + B: SpiBus, + K: InputOps, +{ let pins = Arc::from(Mutex::from(pins)); let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; let input = ST7735SInput::new(pins, keyboard)?; diff --git a/std/src/spi.rs b/std/src/spi.rs index 31c498b5..d6faaa7d 100644 --- a/std/src/spi.rs +++ b/std/src/spi.rs @@ -15,7 +15,7 @@ //! SPI bus abstractions for EndBASIC. -use std::io::{self, Write}; +use std::io::Write; /// Defines the SPI clock polarity and phase. #[derive(Debug, PartialEq)] @@ -30,9 +30,6 @@ pub enum SpiMode { Mode3 = 3, } -/// Factory function to open an SPI bus. -pub type SpiFactory = fn(bus: u8, slave: u8, clock_hz: u32, mode: SpiMode) -> io::Result; - /// A trait abstracting access to an SPI bus. pub trait SpiBus: Write { /// Returns the maximum transfer size for the bus. From 854056555af5fe7b5438031941b2b7cb30b2634e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 21 Feb 2025 06:59:38 -0800 Subject: [PATCH 033/110] Fix choco installs on Windows Apparently Chocolatey now does not accept empty checksums and the packages we try to install are not compliant. Workaround this issue. --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4808f4fe..9147fcaa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v4 - - run: choco install unzip zip + - run: choco install --allow-empty-checksums unzip zip - run: ./.github/workflows/setup-sdl.ps1 - run: ./.github/workflows/release.sh windows-x86_64-sdl shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 434c29fa..a79686d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,7 +81,7 @@ jobs: TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - uses: actions/checkout@v4 - - run: choco install unzip + - run: choco install --allow-empty-checksums unzip - run: ./.github/workflows/setup-sdl.ps1 - run: cargo test --package=endbasic-client -- --include-ignored - run: cargo test --package=endbasic-core -- --include-ignored From 1916c809b5c2c58ca95d373f4456813d0c545217 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 21 Feb 2025 07:59:43 -0800 Subject: [PATCH 034/110] Fix packaging test after addition of st7735s Commit 6109c2099ab09c13067555323ff81a6bb873809f added a new st7735s crate but I forgot to add it to the post-merge package check, so it failed. Fix it now. --- .github/workflows/package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/package.sh b/.github/workflows/package.sh index e6ef03ba..1ef08139 100755 --- a/.github/workflows/package.sh +++ b/.github/workflows/package.sh @@ -55,7 +55,7 @@ EOF # Package one crate at a time and add it to the local registry so that subsequent crates # can pick them up. -for dir in core std repl client terminal sdl rpi cli; do +for dir in core std repl client terminal sdl st7735s rpi cli; do cd "${dir}" cargo index add --index "${registry}/index" --index-url https://example.com cd - From 4ac5420a650862cbe2c65bfbe63ea91c56559413 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 22 Feb 2025 08:47:13 -0800 Subject: [PATCH 035/110] Fix handling of extensions in LOAD/SAVE/KILL The path representation that EndBASIC uses is not a Rust Path, which means that using the native PathBuf to query and set file extensions was wrong and could result in unexpected behavior. In particular, running SAVE "LOCAL:" would result in the SAVE command creating an empty-named file that the KILL command was late runable to remove. Fix this by moving extension handling to the storage layer. --- .NEWS.md.swp | Bin 0 -> 45056 bytes NEWS.md | 4 ++ std/src/program.rs | 69 ++++++++++-------------- std/src/storage/mod.rs | 117 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 147 insertions(+), 43 deletions(-) create mode 100644 .NEWS.md.swp diff --git a/.NEWS.md.swp b/.NEWS.md.swp new file mode 100644 index 0000000000000000000000000000000000000000..ce8691023219d850e465df62fa2a9c666ac1b879 GIT binary patch literal 45056 zcmeI5d2nP|b>VgQu zG2sYx#}_Io^WEj#bI(2NeQOif%^gp!7@w^0-)~hazyFKB+IacDy+h@zQ&(1QZnsbA z&%f-yM)oUxt@2bgc%Yjf$U2R+G;h`qEX-cNJl<~nvJUn>-=n}D1%5pW4EpQU%kNsb z_~7J(8oJ>82PgNs=jyN5=HAnL6xgG{9tHL&ut$MC3hYr}j{Ia9Px7qdcw&C}yZsmKo3x9vyuAi};+lRj|g`a;Oe#Xy#bJyp02p`1H?+`ve z5q>^)*XMT(f3Jm~Z`$?wox~p~z!>-({)G2|TR z^}4&j(vu%vp09XZ3D_f24WVh@^lT%}&BVPBmHB z?s}y%ZB%15G>%ivUVo6bl6G3(WMV9H+S$nR!#=_7Yz-oJ@@^|kZex)4Qidtock(8y z*@48_W(pO(LB_&wuu!Mb>}B=9Z_HkJo6w&0cQ!WVX@`M&>Nk?8SjYE88Lr9(HFp403Hb zbG^w#LI)ZSTV;z)+=q+UWG(n8PsSL@Qf+@yyJmL&1V2Tk z+Wg$|N^M`UWb4h-Gz7;rcxFSqp=X_D8U<3My%iUP!PJ|1#y%BWbs>$RVE3daQk|-R(Dg2qs(B(oQGq?N7XWEnU`V(js3)iu4D$4{j{yLX;;Wxfe3s6h^wG|#g(eIRpLNC|NGJq=^liK3wvGV$Qy#G5Yj)<&zlhI}c` zMx=MD9e+GozGiA^cBVG&BWz@CSgPNH0(laKu#;?edna8S)Q+w$Os~u>vRiChF0URD zQFL^)y7iOV?<)-2<;m>v#pT0C=Z-8cOifSEjUSm>t|jU~yHjWUuQ2X&oP+sbt%n?C z>Pd&Cr{~(WbMy1HWZRBgTNDV{qeIpLQX&D?o9G2ex-LGc_cCL!W*nsl1+3av^HXEoBG-qMO84?dna95wr)aJ@TVH!_?i zN7b(La2>&Dd^O+f3{F?`t*nkvu50saST=NLpVU{e=(IR(tYc9CzHGLo!TBC(^kDj4 zJdB3pmTs8TI4ZA_+L4v^YB(@!)oM5|u7WMLHkU0#o z!>8e9n=-6-%*QQtNSi?#ExL*`c$D>Os;nvp)ui5q%}i{#s6ZQ|5&p*Ncanpr!_F)Y zP0=L^LzlFOh^02NhKIqruKd*T}g92+360XxFpCH2EI0Ewi+By zD0rMQ9FL~^BCZkiM({+u$hp?b>>CyB(zJCl>{dj5cff(<3N4E^H|#)bBi+!}VLh#9 z;SfX&4_Xb|=4i-BC)!7?LSXp$5B4~6K6-P;X|B|}4MlUo{@?PWXmG@GRX zgqS)pw~vWl7mkh2ppAOyqflGPY^Q-#o|}en9mb88Z8k^KY_d*JT4_YagKZCZ)06`j zK1nir8UfNDCCrLVs6OcRIN4@QaI-Cg{ z!9fZ~U8p!eIJo5*8As*0Xvt4sIm42J69*@%6Njn?59;{3b+ure5)Y|v*g084Cxa`t zd9#sKv-Ndzx}Xo}xIhzVp=M%N);GHxN+<-b!L-wpDEC-)Jj77ZF|0|`ucS0_wwu;2 zH^z2HirN6@RPg(q%(5-FNz1C(dJlhW0RK?K);QNS9uEiJ#4*8hp+-_c?Vm z5~+I92+UigvEi56z#6f66~x*^!{rEe=O$xn$R@?*1fgBsH8)6@6kb9ci*nCdyKeNX zb@Ak(j|M`@rNF;)T%mZ#IOFl1&{oNOFftuq#JuUE^hRf+?Ce3X7qHA2XoH3r5lxHm zYPQil)s&e8GeLAy?NH_eCDRsWTSFckbDW@@*806cc3>R`9;Ss^7?>5N!;elXxb~9T zPL?0&bi?B99O&p@J`t&x^`2I@ru(>&~R$?=(9BWk*~^nk$EN| ze+%W2CE%f>a{L7)5~Wtkl1LZTA7@#ZiocgRQ|l193YV+qTXHB=tD9MCD@?Ik4GJYr z6s%de+_phv)N1gYF&rtr3<*OQecNDr8{5#sHbgbFIHG@|F zaAia-b~lpdv)=3t6?0~Y>KcfohdX$f%?TsNqYcQXw8E3!P89>^WON$HQ`p=Zgt$Rr z6-A%nZi_lFk;qkBT3x`;AyTQ`9KRjx6MPZ3*6i%e+=_gW=6~cT^`H86X?9`tcx^by z+AjYPB;IZk`-skQ^C=}`h5v;eTnxnhoN+m{I*jQQtmO+{IFn>Y2aiQ1?QNLjOb(Yt zi@U&#eX~Rme63b@o25zC@o*q!+)9*zoJM!mVT`S%*%R}!4sOKAlBvuX@!XmaVn<`z zZ=t*s!BI=K=mx;Of*(<$4a=jl5*&qRuCRFRrCgV*TbYSmj^_~)gr2&+0W!;_23of?B`CicEfh{Q_8;to*GY$sZMu=MYn2J0_5BnOv}l_Tsycp_kQ+Y{QeYpqojwF>?~*P%nu# zclZZh+2y{<_u+;>-s9ixam1VBF-XcF1l{dco4tW;tGSERgvDwTCWMWCr{(;`OB1%B zKS9v2zlj4G^E;kbM!R~tr7e^w3boF*;z`0&7LDWig)X)$>$)~zv*EB-^lK=J#e}_V z4GLp&mz@it=jdk}(ga>WE^cO7-yO7yfF$!%3&)IeS(B2CpP8L6{Dqn+hYF{<&KZJe zgh0!xqS3TT{cE} z77pVT_atcnY9>?T=URG}$&ddiDb@$ARyD^%D}OzH(-3A+`)2>Y5Ignd*t3fN$Nc_Z zVC#Pk`~&z9_#k*Xcp6v(DVPN_;2@BH;H!7z{s7PcZ7>Ed1fQ494?YT>0v!ARZ+ zZm^JScJR=phZFaoG`a&hLW+b~k4J>GjO%gpceiu-PCOy_OfMY$Us0VrbWSfE{d4J` z;{ORs-fPK0m;E2>_uq#-e+%e?3&5wbk&@KtR5uYebUd2lWGIrjdyz&F8rz~6u#mo z7d!`C1?~*)1l~iwL=V)#8n^}=13x>jQh5{jBk+e{2K;aG{J#Uf4PFWA;E~`e@Lgyw zy1z!WKgWM^9};gO4bXxM@SQw6xcG5MTE4GknXl# zEhi7LM6R2ZNB9t2Z5lqDOxmL*{^6X8{`TnRq-nXy`kSQuDi_p;A`iX!|E5dHvb(a! ztnU%WU7xsRk~U6}{4>mRPikaK`fz<~g?$La_P@FUZxa`#)!wI0dfG2|8s7z!`2&%=LJC*Ps6rdg$dDxbeLwJ;;UE9rCkG4#FOz(2JsLb9Nq z-&X#57=sc>w>Ps+p!&nsZkn8!oKO=FYuq$>@Zv-E&+bi^U7|l~V6}&x=7aSR{BqB4 zU`x939MDomH!Dk~{yI5rgcvNLdp~hz3n;m%dh(z_=Y_Y!V3)Pn^HC-bs?~-1~#Fi`z`%5&EoJXj&UL@R~nvmMuQHG?P zek)1~>_$3czp7MCjtrZVB1Xx~V$CYfh;12MXUzb-Bj z(_LOYKASAe-mp?5v^(w5E9#R}zD7JW&pBek;)aw?m_-AzL=sfSk+b38CS-0?Lu8vQViV zTeLz0YjkOLWp!x*$>v!6+wp#_oNUdrM?G3;+f&mexTy*(v7&|c zUCQed6cv@V8Slh|Sq7<(6vKt8D`e7;JJY66Ogz1gY(%#hd7`2v38@7IgBeBq7q#GMANx?>3)`grPk;oc*%%E96dBl=+8{J{j6K4dOB)JL>tpY*AL^64Z zl4;Z?Clakl!U-|5w3l3RwxvCPWIRYgid(W)e*XUCW+f>pHz|lu9V@ofqw?{Ri_g}q zw~~yN@@1e71(B-aQu+|$xJUF> zD4VUl+oU>Jojh1QcL)dTC9dT84TJN1BQeD~pv+5i6RdL7*56h~{gx5E-iIAsGkVh^Z8%yO7x0R@D+)BGweU z1`MZCGXal~VbDi2kg-80sy?+KZYD9IV=7OI{#VGUhSxPtDu& z4E=OqWnyMSNFRyixk2p+>Uz*_s#XA3(n%3m7a_bZD)iQ&iZ$3myi=9~A%@~Z%%!%X z)?#(yxv50{+!uTV`~LOdb>Io$UxO>bVelPn{MUddf-Uec@KEpt?EF6f zj|Yzf_W(b`&i^TR3sC<4Jh&E|2Yv(mH8y?=JP7;(+y4K8w}Bei2gbmcu<2g_9t~8> z{}F8X4}-UWH-i^|-v_Ge|1oy_8^II6A#gExAb0@y*m>kHfEG9j9t1vx9seBgY;YKS z3p&0GJO_xT&jP=~f7%aTP8UvbiB$bD6t#uj6-r*D2U;d4Yu-lRmf2@Ew=``mb4uqV zI${~k7S5z*-I^=sP35o%%r9{+nSw}1HpqU&@YcVjlZ0YkXK%5XWd@4bjf+RMI9S$m zu}V`>+Ec5$LD@3}bSU&nTnv(^W)(z-4;-jD7i-VL@d?VLODEbZvURonL#g1@laxX< zH6sfc!%Y^j8w(s_!LGlCI(Rzd=$D(!B(8ejy+=05YD-(x-%1iFIfe;tbkkO-m~so# z=V)_Zo;$X%dSb-gV0DiW(GCS>uhl?*pd^^v17dZcas$VQTQztcqLPHDmJVWbmoit( zPf0?fb2Q2^C&{K_UTpFdW29#*vbxnkH%FTYRoK#dl)qi5!g>_Z6ANE8ui03)G*Z%a z>nEKj6c^|9$a!e8Mj498C=4I8U|4)w zNOemWi-!{II_Q*0L&G}PBoW~@R@GTv$2ZP0KDE8&e3xgHk$FbdSLcEGx+sY2n|y>) zyM`qpU+R`gqW9?hXsQ6-c*n`Y8bu9=e=@45fVb_Ak3g5mlW_sgs`gA=TAjG$oSxh( zqSg~sWb8zh*(rnHwKfcuQ2WL%+5}nvJEoimhYL|;x@pYXj2zNd7}sJ`K%ik^XOt82 zo!A0NT1|(Zh`*@V!=9jkqNuX`nplCl0u0z#Nj=4uOm& znoM5~id0O^^^)V6sg?Dgy1;O!wT47RB*Kzo?$PRUdN9vr+ znV>ohuOU&h(g7(a@YA34cjGq^l%mbMPYHS{pJuc5ofIRhduHahKjS&85X1Cxb4DSeqYv7>2yPU>lBNSPNPMYsK%p2TFlEM> z-z7WERV#L~D2+0lD1K>0knaU?ZT-V>R68+;g&mD5_?(0e-(ET|l~6mjPYwpNq0C^Q zyRzMRM3$;8{`8r?aCWMEz{QNJRONFf#A|NoINX|pOZnQ_8_k>G{A_G5Up}`ErAWPa zQA6p-J25psyRtGnylxQg_doUx*yI^r9G*1EVpj%5*FliQxu+gauPn_! zd|IwHTGF>U5u%wmtWfa64YwbncgabjO<3H_6g))`Mz_JBdlXgaR;FB=$;hC_XG_U} zR)S^W8z;HiVq_YHYr&ip>Hta(+6Z%^OLoJQC^G8lk_eW7U<;6bOG1VlUi4I4Q)Pyb z3AT~!Z7o3U*wGtqnx0#tx+A!>hJQF(XjNJ~wAqwnq1j_@>>(psE(cNY5rm5k&})M5 zb1H>T6+)2HDGEl-t?2)n*|}ra#9}k`fBoFd$~6`tz!{(-cv!bgsENy?560S9p2jtJ zJ$G-Y)f&H%=*@4%cGKvKhBynK6gGkvx#`Xjiaw;jBhzTmB!L7;$;)KBeJG}*B+;N; zPQ`+`l0?zz#T`8g2^Nl1pEofz6U_t{d0`mWyHbF##} z54u4|UuOPX=;K#}Fk|;}l)P%{n|Kx%e3Z@$yl+YByQw5%rk1A_W0}6DNS9w;DMd8m z`lD!|_G=B?7_UBYK#)Mk^KiSW?A} ze)ZkZraY{Prpi}UYN^hPgF~_|UIoUMI7juP+y6%~P+u=QmH2 zbHQ`K<>1@c_%8#C;NIX~;1k&PPXo(f2~2@Uf$wA6e+YaKoCagy%h>j++yAfN-QZnd z9o!du7`y&2!C!zs1}_Cq1y2E0@KtR6KLWF$3ig99Vdp;&oCG(62Y`=b>pvSj3pBwd zmFY$-U;jV0zG;r8g$$)1*Z*PJmjy&=Uxm{?N+!70^b592>2eG7#E>;m zXfB><=xi3cDy(QWqr_0D7m3jMxrJG6Vg38W+zm*F@XHcGrt-g!@{3s2zPWtk!gS5M zk&I~e>~=1k^ufDvl!BEb%iO^;ugi6G`&WH&*`E(7FfR2&U0!K^2&8Cl6%#<2ladr~ zv*Docv*~m*BI9viAVJ{aCzXK@`A)3~tYuc1l1acvpvM`{@5;gcqA z6>CtWpl+{h`*eQi?MBNbJ_lq<0&V_weP!(W1ekWV=v>sv>H~>yK zP3a-smaHx&=#G_OPpOVi?gvaz23LxQ8LI{FTBYLd9H>PorAh$Pbf*m{#b#j|qI4IB zkX=cbXE=-)m^qokx|qTfJki)+fiZoZ{VTL$1gb>FiOU58i~MPh7 ztWpe0p4quBE4Vy5FN_!HFby{giofu34w;K<0Y@A+D$7=}t_=!uyEJHXeG&H&c&WAd z4Pb#>3#da?=38B5zTvhaEk1Ya=;~1g66szRJ@wPvEg)Gi5?z)}&3KM=yTmY(DNOAh zwH6tBq$tss4}z~OO-)}ryR_d0oh_Cr++*^tgl&cGA2b(6U{@;2At(-pPq}VQmmVQ2 zJnKuoN)tTM^P&ml=no7&@I|}XXb@%51ftb)2rbr?M>ADI4Y7?x_N(nk+XX~+wNDT< z+EXMNYS)s~v9akh$hqK;&$}YR|%qZu?FD;+GTzQ+Cmnc!UyCr-HhPA>u_iTFYvN(uKHaYi( z71348f70@DMS(0h4kexNoWUD*39z-U&!fhRKrla?$>noh0%jCc6x0NVt^v^~M~+p= zkKsst{Bf`do66L#OFUXaa!>TkF=x4roG3BW8McO2V|7lh zD--*$Bg~-6jR*yrmmMI3ksoDFjW}08q?UdDrg^CEjj(PZv!*PBjv6rUF@pPJb)csOsgwL ztCzd>7FqaGJQ-0YD1Ad7ye=6$T$)0rkLMhR?iInXzI`!@dg8JdU_F#Z%IP7))?f7T4Fqc|+s7?mmrj-FU9Jn^81~A)l~5j4h1N zF2Y;Sl$ly14`7pPVzw1+AsM8`sl{GtXEkdtsGO3C^4l3{eNQmhDdagOI){`kv$Ttv z5sx5UZ03NqU)s@8)7IW4CAF%l!+dDZDAz>@%-0}v=;T)352?iJYbei3bSM7vOJ-yo z8AxrjOOw^5`O)!j_WN5o zt4`Lb8$=WYkQNc6bA=VJO9vI))zM|dcTKw#&*wz(YkY<#iWW&NE$Z^5z#Cdc=}>10 z!r*^3|B!D>kFi?(|6gIBc4enx|DWK8{(CnzzheLQ54Qh1u=(E(YT%yWBiQ@8>;EEf zcW^iGUTpm*fkyzv`epYk&VLFFz~jN=z`enTu=Ae;9u4Ne&#~*@4xSF42ByIg@K4zG zZv(FauLREozYBDazheHs4OGwn8K4~h7lJA9Lu~xlgO`981Ni`+39ba+!q$H|XoD6= zz-O@cp9`8`6U>8a!2vK1zI+~c5rC_~UBO+z53%)s0A33o3+@VTfsUeO=j?VrqS0fK zR~qlKdUiQ(bgB=frQgvhWl%2iu?)^Y65(;TyE}`!w~gy@I$4Gm)*9DwS9OT6_bnmd zN;&IvaWICr+Y%fcx?dNO4EHD~H+9sjC1ILRdGn!d{%6!^>~T zo~MhY8;5-$(^6~9(hWw$$;{H+b+d;LoS2f&b$AMU_5!YD-Gt*(+jT{{I*Y84yDZYi z$HZ`_*t*tXp|yz9y&k{?n?w}usdA#T?n3ew*r6kSY`$y}RMBaBQvtT&^-Ub{u3;2i z#jAfgytw&=;km#ojtHeRYGynTHo9FOtusNly_+AsQnt@V?p9sV&C9$QfJH+EaqAIL zF*MXty~e;UcJj&Km*gZfZA^VX`o*j*6D6EsJ zbeg#MoF?97(Zq|q70p%aQtlomb*}vcS^h$@`d9OQq_SHM<>QYpoZ(4X|6bfVqhAY z`yqFtS-6B?4xTfEx|r7)w^7>-L9U#X^gb=ona|q&%i)2zjf%Gz7+H$y3|%gSC8^7i zL#hnwz^=H-S^GfgAYC_Y)r>St$OpXyAZS{{X_PDw23jQep2-2+l6`CXyf(94M(!(0 zo)XR)+<10*)1c!w^8|&)cUQEoYRgl0|2L0#g0xmO+@WiYnVu8Btq)yuwq&RfTV#t2 zW3~ut-0yfb?J6Yi%x*6;DvX4oqg*OP1M_!7NO85u7-2B29Xv+l@)da(oTy3kuSU&E z;zVO4{Svv$=0#4(h<1BldcW)H+?>aa&g0(1aay&fjubY99+gpRkGkWFEvHKZITCEM z2jyclqU{Qa)#bzwG{$H}OkQzo!3>RezXrQMIiXW*b;}87zs$(9ppk^-H$B=ap-RET zpfv?gEK{T3#G~o8WeT;zLU4t2DFVk)uwwVqYv*^lou2$Qd-Z~LzXzyGL>6H=BdGuo zd1_0FvT&eobUVsuPzgz~ka0`ZrdHH7S3GgkQ+dIxEegntQ~0TiC?kaiOj0~;` z4Z4K+;eyjh1Phj|9+L~w(-AaUUD^j_xC2haP}!H7#JQ3MD^O1#J2AItM09(%UrHno z1mP&eFgJ=67X})OaN_wTj0i$C6{%@tnT>U2`XtWY)=+mAMiLF(W_)eO#}?0ZVgMPQ z7mGuC24WCZfvJ^)94=Lkv8)Fkev{wrI=H3MIU#!hO8JfkywCUd;Dfz$Yaib<)n)KF^5Fo z@$#B-1()V&9Lg~roOe18r8%7?Cgj2Z?h0h}DKQ9}Z1E`$qcC3iBw7UB%qbzkJ7($; z@>xHX>l#JXzPJrc{pw`8qEIPb1{K&c#HphmqUVV1bp?DNWkhBi^W6OA=VtxAF2WnV}9MJ97}3X z?>lsPwi11t{f|jp`4icz z*#9+t=)cck_kSAvE~tYwa9?m2aA$A}K7by02>2ql{|mvh!Lz_2@OA9}4}uSXKL#%a z*Mc!{A^1E#0KEtBbmb6`$K18RNY|he5bwECFN^)F)nHcDUCb%e&07m`eI1~ zkt*lBBvu^O9!aez&dV};ags|ZmzrX)d?Le|D3Ny%+qHzmLdFx^30CZQPCXjQx3 z>RS=GdOW`5(Ik<)0?MYnUYZqI;bnC-4oL$sPox!mil`_p&NyhOw{DN@=LGWy>-~W%6u)2PdWSC z7_3*hKn*<+Ch7wS>b#w}@x+YpSsZkpsQVb~y?T-P(dw*tpA*~<8MRt`sN+3cqV99Y z*oi8NR!-Lr#}imvS68yzdpse!k+wJ-cPI5p-^HJ<%jLsM62l!E{=6C=4nqoyZ$Wc> zh@_z=7k?2i>-B{3BsR7hw8OguN3R(VV-I=- zY`&sA^AgKyt!m5jv$LaBJ6l$8P_nPs0b~-B^o>=c&ED+V#*RmvvKC!|$#$4894D%lgkY1)+)H=>%D`e8j3o*IAOu)$f zPmHhOQpI>+b!#Xn4uyx-#Z8D^wlQlvrqE!pn1!sDtVY4j#&=JfHB{5Sy6O6*xl(4R z?^mO%ed>9M@p)SUy&Eo`Mf6Zw4hz0SQWl%sQl!HKNDWh;%ao<-ZI%9;8=o9v5+Imq zqc@F2L4sX1LP8w9_Fl@vBOQ+ImUMcttIgUgQEm^y^g6-kTu z^rAE?L-WW_lJZhWi)pv>tVK9w_Ze5Smq?DtKNX>iY*24%dfrk?tfGxe5J*n3${mR4 zg39_Vw;4RlF_2sHlF@jWgep)ZX}q~c%;Ql6QNgl@y=A^*)utwF)D4G)ym+I45%gXaE81q}_Qf^+Mv8FEqvkj(k9q*@rNbp&cD|rM zEW8sJF-7JCYp!)I!n6Frn|1M`&w}IM0=ZhSGxp6ihe4?$cN$u=@loR@n}Hgvow$DF z3GWbb8wMknSV(qbzmCL}NjlX1f0(nC_sQPG{%^|cM!9QX9zX9lOfE&Rim;hhL_Wx_}SKzJS zPr*$<_W>RT-@^WXCHObs$>1jN1aNQg5p4bEfqw_C26qN`0^h~X{|@-1?0xV~@D8v6 z^ge(`fbU}We-V5Ed;q*3JQX|zTn;V+dOyJDfcyim0j~zvfy;sX1mA;zuK_mz`3L?p zw*RxhGr zKT57!3&%)+`MU zcO_K%TT6Ky)1tqPl;vWUaI|ydniq`j~P#B;tNd_e9DUtab3Bf& zROq?1vV^Z{WMF7aOt8=aIEsr2sMYz|qyDP0S&Q|~TLTK`&JoQm4=T=3(^DO`ac-b7!zNy1Wz0j$o(Yaci2B!>23bqY5cuJSzbyoquOLD!4f8+&5?(iWy1+ z3Vv*78@I}@B<{w{@s!v?y|pBp5rR^=W7q8P21QN@wL|2xZEwXao5Quy#2C-F(i2Y2 z!1OMgvXHG>wP%oTiipErY?$3n5VxBP|Nbx!$KD#op2D2y=voQ(VQ9$ODfq@tTLb{- zuW`v)-Z7@J?e146Hu4vVT%2T1n!{=yH-=qzVvWnGte0O8&oBwL>3*;E@M%j#BG5^d zI$6JR6`W2b(0p2W<`kl%BTv5>Mg`^M|ND*pkO18(51EbY@#WpNCN9<7^4Q$?Lr)_1DTq7OR$C zk2s93?7E&Z#_VW4*84zyWmhpF4Z~bGZaztnpLuAKzqO^rGxsOR-n2Qn2IY3>+4dd; z(Kvm`9FLqJ#@{`ssP_DUQgb#MpS8wDUocW(^jb=b#O z!iRlpykK^$8LJG;h%U~cLPRY=O#*DFf@nf;)fC*u7D4c+j0~Eaa2t{0!Dmb75?o@Q z+SKaG;_MBx(?fYE&zeor7*4TucQZ?npF^@0yGY_9IM70%S$VHX!%k< zVk&7<9}r!zGM%d5`e;?Js$hkH$Jw1xlv)8 zdaF%i=XkS+s%S?w<-!+VN)#%jCCZ2LB8zBejheO%UG4#GTCLix4)5HD%O$a1%VZRe z4J3+PtHA=;N}2c!v0Dxr2{pojrCJj!1`w0FJS>z7*Exg2l~nBoWt?|_!_S^0kx(3`3N literal 0 HcmV?d00001 diff --git a/NEWS.md b/NEWS.md index 9f8e8951..c12e204a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -28,6 +28,10 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. ST7735S LCD, decoupling its implementation from the Linux-specific bits in the `endbasic-rpi` crate. +* Fixed handling of extensions in the `LOAD` and `SAVE` commands such that + paths like `LOCAL:` are flagged as invalid due to them not containing a leaf + name. `KILL` was unable to remove these files after creation. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/std/src/program.rs b/std/src/program.rs index dca447b4..ee251490 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -26,7 +26,6 @@ use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder}; use std::borrow::Cow; use std::cell::RefCell; use std::io; -use std::path::PathBuf; use std::rc::Rc; use std::str; @@ -47,6 +46,9 @@ from."; /// Message to print on the console when receiving a break signal. pub const BREAK_MSG: &str = "**** BREAK ****"; +/// Default extension to add to file names. +const DEFAULT_EXTENSION: &str = "bas"; + /// Representation of the single program that we can keep in memory. #[async_trait(?Send)] pub trait Program { @@ -106,30 +108,6 @@ impl Program for ImmutableProgram { } } -/// Adds an extension to `path` if one is not present. -fn add_extension>(path: S) -> io::Result { - let mut path = path.into(); - - if let Some(ext) = path.extension() { - if ext != "bas" && ext != "BAS" { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid filename extension")); - } - } else { - // Attempt to determine a sensible extension based on the case of the basename, assuming - // that an all-uppercase basename wants an all-uppercase extension. This is fragile on - // case-sensitive file systems, but there is not a lot we can do. - let mut ext = "BAS"; - for ch in path.to_string_lossy().chars() { - if ch.is_ascii_lowercase() { - ext = "bas"; - break; - } - } - path.set_extension(ext); - } - Ok(path.to_str().expect("Path came from a String").to_owned()) -} - /// If the `program` is dirty, asks if it's OK to continue on `console` and discard its changes. pub async fn continue_if_modified( program: &dyn Program, @@ -262,8 +240,12 @@ impl Callable for KillCommand { debug_assert_eq!(1, scope.nargs()); let name = scope.pop_string(); - let name = add_extension(name).map_err(|e| scope.io_error(e))?; - self.storage.borrow_mut().delete(&name).await.map_err(|e| scope.io_error(e))?; + let full_name = self + .storage + .borrow() + .make_canonical_with_extension(&name, DEFAULT_EXTENSION) + .map_err(|e| scope.io_error(e))?; + self.storage.borrow_mut().delete(&full_name).await.map_err(|e| scope.io_error(e))?; Ok(()) } @@ -406,11 +388,14 @@ impl Callable for LoadCommand { .await .map_err(|e| scope.io_error(e))? { - let pathname = add_extension(pathname).map_err(|e| scope.io_error(e))?; - let content = - self.storage.borrow().get(&pathname).await.map_err(|e| scope.io_error(e))?; - let full_name = - self.storage.borrow().make_canonical(&pathname).map_err(|e| scope.io_error(e))?; + let (full_name, content) = { + let storage = self.storage.borrow(); + let full_name = storage + .make_canonical_with_extension(&pathname, DEFAULT_EXTENSION) + .map_err(|e| scope.io_error(e))?; + let content = storage.get(&full_name).await.map_err(|e| scope.io_error(e))?; + (full_name, content) + }; self.program.borrow_mut().load(Some(&full_name), &content); machine.clear(); } else { @@ -602,11 +587,13 @@ impl Callable for SaveCommand { scope.pop_string() }; - let name = add_extension(name).map_err(|e| scope.io_error(e))?; - let full_name = - self.storage.borrow().make_canonical(&name).map_err(|e| scope.io_error(e))?; + let full_name = self + .storage + .borrow() + .make_canonical_with_extension(&name, DEFAULT_EXTENSION) + .map_err(|e| scope.io_error(e))?; let content = self.program.borrow().text(); - self.storage.borrow_mut().put(&name, &content).await.map_err(|e| scope.io_error(e))?; + self.storage.borrow_mut().put(&full_name, &content).await.map_err(|e| scope.io_error(e))?; self.program.borrow_mut().set_name(&full_name); self.console @@ -895,12 +882,10 @@ mod tests { .expect_err("1:1: Too many / separators in path 'a/b.bas'") .check(); - for p in &["foo.bak", "foo.ba", "foo.basic"] { - Tester::default() - .run(format!(r#"{} "{}""#, cmd, p)) - .expect_err("1:1: Invalid filename extension") - .check(); - } + Tester::default() + .run(format!(r#"{} "drive:""#, cmd)) + .expect_err("1:1: Missing file name in path 'drive:'") + .check(); } #[test] diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index d4d05c10..aec89863 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -280,6 +280,42 @@ impl Location { Some(&self.path) } } + + /// Sets the file name extension as long as this location corresponds to a file and not a + /// directory and does not already have one. + /// + /// The `extension` must be provided in lowercase. + fn set_extension(&mut self, extension: &str) { + debug_assert_eq!(extension, extension.to_ascii_lowercase()); + + if let Some(name) = self.leaf_name() { + if name.is_empty() { + return; + } + + if name.rfind('.').is_some() { + return; + } + + // Attempt to determine a sensible extension based on the case of the leaf name, + // assuming that an all-uppercase basename wants an all-uppercase extension. This is + // fragile on case-sensitive file systems, but there is not a lot we can do. + let mut as_uppercase = true; + for ch in name.chars() { + if ch.is_ascii_lowercase() { + as_uppercase = false; + break; + } + } + + self.path.push('.'); + if as_uppercase { + self.path.push_str(&extension.to_ascii_uppercase()); + } else { + self.path.push_str(extension); + } + } + } } impl fmt::Display for Location { @@ -359,7 +395,7 @@ impl Storage { self.factories.contains_key(scheme) } - /// Converts a location, which needn't exist, to its canonical form. + /// Converts a `raw_location`, which needn't exist, to its canonical form. pub fn make_canonical(&self, raw_location: &str) -> io::Result { let mut location = Location::new(raw_location)?; if location.drive.is_none() { @@ -368,6 +404,29 @@ impl Storage { Ok(location.to_string()) } + /// Converts a `raw_location`, which needn't exist but must represent a file (not a directory), + /// to its canonical form. + /// + /// If `extension` is not empty, adds it to the location if it didn't not yet have nay. + pub fn make_canonical_with_extension( + &self, + raw_location: &str, + extension: &str, + ) -> io::Result { + let mut location = Location::new(raw_location)?; + if location.drive.is_none() { + location.drive = Some(self.current.clone()); + } + if location.leaf_name().is_none() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Missing file name in path '{}'", raw_location), + )); + } + location.set_extension(extension); + Ok(location.to_string()) + } + /// Attaches a new `drive` with `name`, which was instantiated with `uri`. /// /// The `name` must be valid and must not yet have been registered. @@ -687,6 +746,24 @@ mod tests { assert_eq!(Some("abc.txt"), Location::new("abc.txt").unwrap().leaf_name()); } + #[test] + fn test_location_set_extension() { + for (exp_location, raw_location, extension) in [ + ("DRV:/", "drv:", "bas"), + ("DRV:/", "drv:/", "bas"), + ("foo.bas", "foo", "bas"), + ("foo.bas", "foo.bas", "bas"), + ("Foo.bas", "Foo", "bas"), + ("FOO.BAS", "FOO", "bas"), + ("foo.", "foo.", "bas"), + ("foo.other", "foo.other", "bas"), + ] { + let mut location = Location::new(raw_location).unwrap(); + location.set_extension(extension); + assert_eq!(exp_location, location.to_string()); + } + } + /// Convenience helper to obtain the sorted list of mounted drive names in canonical form. fn drive_names(storage: &Storage) -> Vec { let mut names = storage.drives.keys().map(DriveKey::to_string).collect::>(); @@ -705,6 +782,8 @@ mod tests { let mut storage = Storage::default(); storage.mount("some", "memory://").unwrap(); + assert_eq!("MEMORY:/", storage.make_canonical("memory:").unwrap()); + assert_eq!("MEMORY:foo.bar", storage.make_canonical("foo.bar").unwrap()); assert_eq!("MEMORY:/foo.bar", storage.make_canonical("/foo.bar").unwrap()); storage.cd("some:/").unwrap(); @@ -726,6 +805,42 @@ mod tests { ); } + #[test] + fn test_storage_make_canonical_with_extension_ok() { + let mut storage = Storage::default(); + storage.mount("some", "memory://").unwrap(); + + assert_eq!("MEMORY:foo.bas", storage.make_canonical_with_extension("foo", "bas").unwrap()); + assert_eq!( + "MEMORY:/foo.bar", + storage.make_canonical_with_extension("/foo.bar", "bas").unwrap() + ); + + assert_eq!( + "MEMORY:a.bas", + storage.make_canonical_with_extension("memory:a", "bas").unwrap() + ); + assert_eq!( + "MEMORY:/a.bas", + storage.make_canonical_with_extension("memory:/a.bas", "bas").unwrap() + ); + } + + #[test] + fn test_storage_make_canonical_with_extension_errors() { + let storage = Storage::default(); + + assert_eq!( + "Missing file name in path 'memory:'", + format!("{}", storage.make_canonical_with_extension("memory:", "bas").unwrap_err()), + ); + + assert_eq!( + "Invalid drive name 'a\\b'", + format!("{}", storage.make_canonical_with_extension("a\\b:c", "bas").unwrap_err()) + ); + } + #[test] fn test_storage_attach_ok() { let mut storage = Storage::default(); From 3feca19e09ac63fbca2639936120b128a2998ee3 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 25 Feb 2025 20:30:16 -0800 Subject: [PATCH 036/110] Be silent while loading AUTOEXEC.BAS I want to be able to provide a default file as "self-documentation" but I cannot do so if its presence causes noise in the console. So remove the noise. --- cli/tests/repl/autoexec.out | 1 - repl/src/lib.rs | 14 +++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/cli/tests/repl/autoexec.out b/cli/tests/repl/autoexec.out index 7fcc4b5c..19d59b5d 100644 --- a/cli/tests/repl/autoexec.out +++ b/cli/tests/repl/autoexec.out @@ -4,7 +4,6 @@ Type HELP for interactive usage information. -Loading AUTOEXEC.BAS... I am within AUTOEXEC.BAS! Hello, world! End of input by CTRL-D diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 7be20a7c..7f5f2f71 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -70,7 +70,6 @@ pub async fn try_load_autoexec( } }; - console.borrow_mut().print("Loading AUTOEXEC.BAS...")?; match machine.exec(&mut code.as_bytes()).await { Ok(_) => Ok(()), Err(e) => { @@ -290,7 +289,7 @@ mod tests { tester .run("") .expect_var("global_var", 3) - .expect_prints(["Loading AUTOEXEC.BAS...", "hello"]) + .expect_prints(["hello"]) .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec) .check(); } @@ -304,10 +303,7 @@ mod tests { tester .run("after = 5") .expect_var("after", 5) - .expect_prints([ - "Loading AUTOEXEC.BAS...", - "AUTOEXEC.BAS failed: 2:5: Undefined symbol UNDEF", - ]) + .expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol UNDEF"]) .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec) .check(); } @@ -322,10 +318,7 @@ mod tests { .run("after = 5") .expect_var("a", 1) .expect_var("after", 5) - .expect_prints([ - "Loading AUTOEXEC.BAS...", - "AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive", - ]) + .expect_prints(["AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive"]) .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec) .check(); } @@ -340,7 +333,6 @@ mod tests { tester .run("") .expect_var("a", 1) - .expect_prints(["Loading AUTOEXEC.BAS..."]) .expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1") .expect_file("MEMORY:/autoexec.bas", "a = 2") .check(); From 0f2e6719959860112c1d37309f60159c8931ce66 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 27 Feb 2025 16:43:59 -0800 Subject: [PATCH 037/110] Sync all writes to the DirectoryDrive I'm finding that, if I run this on NetBSD and I cut power right after writing a file to a DirectoryDrive backed by a DOS file system, the file is always lost no matter how much time passes between the write and the power cut. Forcibly syncing written files should help. --- std/src/storage/fs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/std/src/storage/fs.rs b/std/src/storage/fs.rs index 9a49aef6..6ba79665 100644 --- a/std/src/storage/fs.rs +++ b/std/src/storage/fs.rs @@ -108,9 +108,9 @@ impl Drive for DirectoryDrive { async fn put(&mut self, name: &str, content: &str) -> io::Result<()> { let path = self.dir.join(name); - let output = OpenOptions::new().create(true).write(true).truncate(true).open(path)?; - let mut writer = io::BufWriter::new(output); - writer.write_all(content.as_bytes()) + let mut output = OpenOptions::new().create(true).write(true).truncate(true).open(path)?; + output.write_all(content.as_bytes())?; + output.sync_all() } fn system_path(&self, name: &str) -> Option { From 98433e5e0dac48d7ed2750116025763d52aa221b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 16 Mar 2025 07:28:27 -0700 Subject: [PATCH 038/110] Turn Font into a struct In preparation to support multiple fonts at runtime, make Font a struct instead of a trait, and define Font8 as a new FONT_5X8 constant. Why I didn't do this in the first place? Maybe because I didn't think that declaring a struct constant was possible, or maybe because I just followed prior art in this code and (ab)used traits. --- st7735s/src/lib.rs | 6 +-- std/src/gfx/lcd/buffered/mod.rs | 30 +++++++-------- std/src/gfx/lcd/buffered/tests.rs | 12 +++--- std/src/gfx/lcd/buffered/testutils.rs | 8 ++-- std/src/gfx/lcd/font8.rs | 48 ++---------------------- std/src/gfx/lcd/mod.rs | 53 +++++++++++++++++++++++---- 6 files changed, 74 insertions(+), 83 deletions(-) diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index 8a4c8e49..0a0724f2 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -29,7 +29,7 @@ use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, }; -use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Font8, Lcd, LcdSize, LcdXY, RGB565Pixel}; +use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel, FONT_5X8}; use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiMode}; use std::io; @@ -364,7 +364,7 @@ impl Lcd for ST7735SLcd { pub struct ST7735SConsole { /// The graphical console itself. We wrap it in a struct to prevent leaking all auxiliary types /// outside of this crate. - inner: GraphicsConsole, BufferedLcd, Font8>>, + inner: GraphicsConsole, BufferedLcd>>, } #[async_trait(?Send)] @@ -481,7 +481,7 @@ where let pins = Arc::from(Mutex::from(pins)); let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; let input = ST7735SInput::new(pins, keyboard)?; - let lcd = BufferedLcd::new(lcd, Font8::default()); + let lcd = BufferedLcd::new(lcd, &FONT_5X8); let inner = GraphicsConsole::new(input, lcd)?; Ok(ST7735SConsole { inner }) } diff --git a/std/src/gfx/lcd/buffered/mod.rs b/std/src/gfx/lcd/buffered/mod.rs index fa5c7542..4d1eb72c 100644 --- a/std/src/gfx/lcd/buffered/mod.rs +++ b/std/src/gfx/lcd/buffered/mod.rs @@ -33,9 +33,9 @@ mod testutils; /// primitives are flushed right away to the device; otherwise, they are applied to memory only /// until an explicit sync is requested. The framebuffer is also used to implement all pixel data /// reading. -pub struct BufferedLcd { +pub struct BufferedLcd { lcd: L, - font: F, + font: &'static Font, fb: Vec, stride: usize, @@ -49,13 +49,12 @@ pub struct BufferedLcd { row_buffer: Vec, } -impl BufferedLcd +impl BufferedLcd where L: Lcd, - F: Font, { /// Creates a new buffered LCD backed by `lcd`. - pub fn new(lcd: L, font: F) -> Self { + pub fn new(lcd: L, font: &'static Font) -> Self { let (size, stride) = lcd.info(); let fb = { @@ -63,10 +62,9 @@ where vec![0; pixels * stride] }; - let glyph_size = font.size(); let size_chars = CharsXY::new( - u16::try_from(size.width / glyph_size.width).expect("Must fit"), - u16::try_from(size.height / glyph_size.height).expect("Must fit"), + u16::try_from(size.width / font.glyph_size.width).expect("Must fit"), + u16::try_from(size.height / font.glyph_size.height).expect("Must fit"), ); let draw_color = lcd.encode((255, 255, 255)); @@ -89,7 +87,7 @@ where /// Executes mutations on the buffered LCD via `ops` while ensuring that syncing is disabled. fn without_sync(&mut self, ops: O) -> io::Result<()> where - O: Fn(&mut BufferedLcd) -> io::Result<()>, + O: Fn(&mut BufferedLcd) -> io::Result<()>, { if self.sync { let old_sync = self.sync; @@ -329,10 +327,9 @@ where } } -impl Drop for BufferedLcd +impl Drop for BufferedLcd where L: Lcd, - F: Font, { fn drop(&mut self) { self.set_draw_color((0, 0, 0)); @@ -340,17 +337,16 @@ where } } -impl RasterOps for BufferedLcd +impl RasterOps for BufferedLcd where L: Lcd, - F: Font, { type ID = (Vec, SizeInPixels); fn get_info(&self) -> RasterInfo { RasterInfo { size_pixels: self.size_pixels.into(), - glyph_size: self.font.size().into(), + glyph_size: self.font.glyph_size.into(), size_chars: self.size_chars, } } @@ -456,10 +452,10 @@ where let mut pos = x1y1; for ch in text.chars() { let glyph = self2.font.glyph(ch); - debug_assert_eq!(self2.font.size().height, glyph.len()); + debug_assert_eq!(self2.font.glyph_size.height, glyph.len()); for (j, row) in glyph.iter().enumerate() { let mut mask = 0x80; - for i in 0..self2.font.size().width { + for i in 0..self2.font.glyph_size.width { let bit = row & mask; if bit != 0 { let x = pos.x + i; @@ -479,7 +475,7 @@ where } } - pos.x += self2.font.size().width; + pos.x += self2.font.glyph_size.width; } Ok(()) }) diff --git a/std/src/gfx/lcd/buffered/tests.rs b/std/src/gfx/lcd/buffered/tests.rs index 12f9f44e..a1b23d94 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::Font8; +use crate::gfx::lcd::FONT_5X8; #[test] fn test_new_does_nothing() { @@ -28,7 +28,7 @@ fn test_new_does_nothing() { #[test] fn test_clip_xy() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), &FONT_5X8); assert_eq!(Some(xy(0, 0)), lcd.clip_xy(PixelsXY::new(0, 0))); assert_eq!(Some(xy(10, 20)), lcd.clip_xy(PixelsXY::new(10, 20))); @@ -42,7 +42,7 @@ fn test_clip_xy() { #[test] fn test_clamp_xy() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), &FONT_5X8); assert_eq!(xy(0, 0), lcd.clamp_xy(PixelsXY::new(0, 0))); assert_eq!(xy(10, 20), lcd.clamp_xy(PixelsXY::new(10, 20))); @@ -56,7 +56,7 @@ fn test_clamp_xy() { #[test] fn test_clip_x2y2() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), &FONT_5X8); assert_eq!(Some(xy(9, 19)), lcd.clip_x2y2(PixelsXY::new(0, 0), SizeInPixels::new(10, 20))); assert_eq!(Some(xy(19, 39)), lcd.clip_x2y2(PixelsXY::new(10, 20), SizeInPixels::new(10, 20))); @@ -80,7 +80,7 @@ fn test_clip_x2y2() { #[test] fn test_fb_addr() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), &FONT_5X8); assert_eq!(0, lcd.fb_addr(0, 0)); assert_eq!(3, lcd.fb_addr(1, 0)); @@ -195,7 +195,7 @@ fn test_force_present_canvas_damage() { #[test] fn test_get_info() { - let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), Font8::default()); + let lcd = BufferedLcd::new(LcdRecorder::new(size(100, 200)), &FONT_5X8); let info = lcd.get_info(); assert_eq!(info.size_pixels, SizeInPixels::new(100, 200)); assert_eq!(info.glyph_size, SizeInPixels::new(5, 8)); diff --git a/std/src/gfx/lcd/buffered/testutils.rs b/std/src/gfx/lcd/buffered/testutils.rs index 7ce107a6..2ad1c0b5 100644 --- a/std/src/gfx/lcd/buffered/testutils.rs +++ b/std/src/gfx/lcd/buffered/testutils.rs @@ -16,7 +16,7 @@ //! Utilities to implement tests for the `BufferedLcd`. use crate::console::RGB; -use crate::gfx::lcd::{AsByteSlice, BufferedLcd, Font8, Lcd, LcdSize, LcdXY}; +use crate::gfx::lcd::{AsByteSlice, BufferedLcd, Lcd, LcdSize, LcdXY, FONT_5X8}; use std::io; /// Data for one pixel encoded as RGB888. @@ -80,7 +80,7 @@ impl Lcd for LcdRecorder { #[must_use] pub(super) struct Tester { size: LcdSize, - buffered: BufferedLcd, + buffered: BufferedLcd, exp_fb: Vec, exp_damage: Option<(LcdXY, LcdXY)>, exp_ops: Vec, @@ -92,7 +92,7 @@ impl Tester { let fb_size = size.width * size.height * 3; Self { size, - buffered: BufferedLcd::new(LcdRecorder::new(size), Font8::default()), + buffered: BufferedLcd::new(LcdRecorder::new(size), &FONT_5X8), exp_fb: vec![0; fb_size], exp_damage: None, exp_ops: vec![], @@ -102,7 +102,7 @@ impl Tester { /// Executes an operation on the backing `LcdRecorder`. pub(super) fn op(mut self, op: F) -> Self where - F: Fn(&mut BufferedLcd), + F: Fn(&mut BufferedLcd), { op(&mut self.buffered); self diff --git a/std/src/gfx/lcd/font8.rs b/std/src/gfx/lcd/font8.rs index eecf9fb7..cff0a339 100644 --- a/std/src/gfx/lcd/font8.rs +++ b/std/src/gfx/lcd/font8.rs @@ -919,48 +919,6 @@ const DATA: &[u8] = &[ 0x00, // ]; -/// A small font to be used in small LCDs. -#[derive(Default)] -pub struct Font8 {} - -impl Font for Font8 { - fn size(&self) -> LcdSize { - LcdSize { width: WIDTH, height: HEIGHT } - } - - fn glyph(&self, mut ch: char) -> &'static [u8] { - if !(' '..='~').contains(&ch) { - // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode - // characters are typically displayed. - ch = '?'; - } - let offset = ((ch as usize) - (' ' as usize)) * HEIGHT; - debug_assert!(offset < (DATA.len() + HEIGHT)); - &DATA[offset..offset + HEIGHT] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_glyph_printable() { - let offset = (usize::from(b'a') - usize::from(b' ')) * 8; - let expected = &DATA[offset..offset + 8]; - - let font = Font8::default(); - let data = font.glyph('a'); - assert_eq!(expected, data); - } - - #[test] - fn test_glyph_non_printable() { - let offset = (usize::from(b'?') - usize::from(b' ')) * 8; - let expected = &DATA[offset..offset + 8]; - - let font = Font8::default(); - let data = font.glyph(char::from(30)); - assert_eq!(expected, data); - } -} +/// Small font for tiny displays. +pub const FONT_5X8: Font = + Font { glyph_size: LcdSize { width: WIDTH, height: HEIGHT }, data: DATA }; diff --git a/std/src/gfx/lcd/mod.rs b/std/src/gfx/lcd/mod.rs index e4ea98eb..f9674f97 100644 --- a/std/src/gfx/lcd/mod.rs +++ b/std/src/gfx/lcd/mod.rs @@ -23,7 +23,7 @@ mod buffered; mod font8; pub use buffered::BufferedLcd; -pub use font8::Font8; +pub use font8::FONT_5X8; /// Trait to convert a pixel to a sequence of bytes. pub trait AsByteSlice { @@ -42,15 +42,30 @@ impl AsByteSlice for RGB565Pixel { } /// Representation of a font. -pub trait Font { - /// Returns the size of a glyph, in pixels. - fn size(&self) -> LcdSize; +pub struct Font { + /// The size of a single glyph, in pixels. + pub glyph_size: LcdSize, - /// Returns the pixel data for the given `ch`. + /// The bitmap data for the font. + pub data: &'static [u8], +} + +impl Font { + /// Returns the raw font data for `ch`. /// - /// The returned slice contains one byte per row, and each row indicates which pixels need to be - /// drawn from left to right. Only the first `WIDTH` bits in every row contain valid data. - fn glyph(&self, ch: char) -> &'static [u8]; + /// Each entry in the array corresponds to a row of pixels and is a bitmask indicating which + /// pixels to turn on. + fn glyph(&self, mut ch: char) -> &'static [u8] { + if !(' '..='~').contains(&ch) { + // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode + // characters are typically displayed. + ch = '?'; + } + let height = self.glyph_size.height; + let offset = ((ch as usize) - (' ' as usize)) * height; + debug_assert!(offset < (self.data.len() + height)); + &self.data[offset..offset + height] + } } /// Primitives that an LCD must define. @@ -171,4 +186,26 @@ mod tests { to_xy_size(xy(10, 20), xy(14, 26)) ); } + + #[test] + fn test_font_glyph_printable() { + let font = &FONT_5X8; + + let offset = (usize::from(b'a') - usize::from(b' ')) * 8; + let expected = &font.data[offset..offset + 8]; + + let data = font.glyph('a'); + assert_eq!(expected, data); + } + + #[test] + fn test_font_glyph_non_printable() { + let font = &FONT_5X8; + + let offset = (usize::from(b'?') - usize::from(b' ')) * 8; + let expected = &font.data[offset..offset + 8]; + + let data = font.glyph(char::from(30)); + assert_eq!(expected, data); + } } From 9624ba5740ff8531d6afe77747c58b171352451d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 16 Mar 2025 07:52:34 -0700 Subject: [PATCH 039/110] Move font support into its own fonts module --- st7735s/src/lib.rs | 3 +- std/src/gfx/lcd/buffered/mod.rs | 3 +- std/src/gfx/lcd/buffered/tests.rs | 2 +- std/src/gfx/lcd/buffered/testutils.rs | 3 +- .../gfx/lcd/{font8.rs => fonts/font_5x8.rs} | 3 +- std/src/gfx/lcd/fonts/mod.rs | 75 +++++++++++++++++++ std/src/gfx/lcd/mod.rs | 52 +------------ 7 files changed, 85 insertions(+), 56 deletions(-) rename std/src/gfx/lcd/{font8.rs => fonts/font_5x8.rs} (99%) create mode 100644 std/src/gfx/lcd/fonts/mod.rs diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index 0a0724f2..5ac72b38 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -29,7 +29,8 @@ use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, }; -use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel, FONT_5X8}; +use endbasic_std::gfx::lcd::fonts::FONT_5X8; +use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel}; use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiMode}; use std::io; diff --git a/std/src/gfx/lcd/buffered/mod.rs b/std/src/gfx/lcd/buffered/mod.rs index 4d1eb72c..4c7f5b1b 100644 --- a/std/src/gfx/lcd/buffered/mod.rs +++ b/std/src/gfx/lcd/buffered/mod.rs @@ -18,7 +18,8 @@ use crate::console::drawing; use crate::console::graphics::{RasterInfo, RasterOps}; use crate::console::{CharsXY, PixelsXY, SizeInPixels, RGB}; -use crate::gfx::lcd::{to_xy_size, AsByteSlice, Font, Lcd, LcdSize, LcdXY}; +use crate::gfx::lcd::fonts::Font; +use crate::gfx::lcd::{to_xy_size, AsByteSlice, Lcd, LcdSize, LcdXY}; use std::convert::TryFrom; use std::io; diff --git a/std/src/gfx/lcd/buffered/tests.rs b/std/src/gfx/lcd/buffered/tests.rs index a1b23d94..8ae9b234 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::FONT_5X8; +use crate::gfx::lcd::fonts::FONT_5X8; #[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 2ad1c0b5..e40308a5 100644 --- a/std/src/gfx/lcd/buffered/testutils.rs +++ b/std/src/gfx/lcd/buffered/testutils.rs @@ -16,7 +16,8 @@ //! Utilities to implement tests for the `BufferedLcd`. use crate::console::RGB; -use crate::gfx::lcd::{AsByteSlice, BufferedLcd, Lcd, LcdSize, LcdXY, FONT_5X8}; +use crate::gfx::lcd::fonts::FONT_5X8; +use crate::gfx::lcd::{AsByteSlice, BufferedLcd, Lcd, LcdSize, LcdXY}; use std::io; /// Data for one pixel encoded as RGB888. diff --git a/std/src/gfx/lcd/font8.rs b/std/src/gfx/lcd/fonts/font_5x8.rs similarity index 99% rename from std/src/gfx/lcd/font8.rs rename to std/src/gfx/lcd/fonts/font_5x8.rs index cff0a339..9dde31f3 100644 --- a/std/src/gfx/lcd/font8.rs +++ b/std/src/gfx/lcd/fonts/font_5x8.rs @@ -52,7 +52,8 @@ //! Small font for tiny displays. -use crate::gfx::lcd::{Font, LcdSize}; +use crate::gfx::lcd::fonts::Font; +use crate::gfx::lcd::LcdSize; /// Width of the font glyphs in pixels. const WIDTH: usize = 5; diff --git a/std/src/gfx/lcd/fonts/mod.rs b/std/src/gfx/lcd/fonts/mod.rs new file mode 100644 index 00000000..8be2447b --- /dev/null +++ b/std/src/gfx/lcd/fonts/mod.rs @@ -0,0 +1,75 @@ +// EndBASIC +// Copyright 2025 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. + +//! Support for bitmap fonts directly rendered onto an LCD. + +use crate::gfx::lcd::LcdSize; + +mod font_5x8; +pub use font_5x8::FONT_5X8; + +/// Representation of a font. +pub struct Font { + /// The size of a single glyph, in pixels. + pub glyph_size: LcdSize, + + /// The bitmap data for the font. + pub data: &'static [u8], +} + +impl Font { + /// Returns the raw font data for `ch`. + /// + /// Each entry in the array corresponds to a row of pixels and is a bitmask indicating which + /// pixels to turn on. + pub(crate) fn glyph(&self, mut ch: char) -> &'static [u8] { + if !(' '..='~').contains(&ch) { + // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode + // characters are typically displayed. + ch = '?'; + } + let height = self.glyph_size.height; + let offset = ((ch as usize) - (' ' as usize)) * height; + debug_assert!(offset < (self.data.len() + height)); + &self.data[offset..offset + height] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_font_glyph_printable() { + let font = &FONT_5X8; + + let offset = (usize::from(b'a') - usize::from(b' ')) * 8; + let expected = &font.data[offset..offset + 8]; + + let data = font.glyph('a'); + assert_eq!(expected, data); + } + + #[test] + fn test_font_glyph_non_printable() { + let font = &FONT_5X8; + + let offset = (usize::from(b'?') - usize::from(b' ')) * 8; + let expected = &font.data[offset..offset + 8]; + + let data = font.glyph(char::from(30)); + assert_eq!(expected, data); + } +} diff --git a/std/src/gfx/lcd/mod.rs b/std/src/gfx/lcd/mod.rs index f9674f97..07e16148 100644 --- a/std/src/gfx/lcd/mod.rs +++ b/std/src/gfx/lcd/mod.rs @@ -20,10 +20,9 @@ use std::convert::TryFrom; use std::io; mod buffered; -mod font8; +pub mod fonts; pub use buffered::BufferedLcd; -pub use font8::FONT_5X8; /// Trait to convert a pixel to a sequence of bytes. pub trait AsByteSlice { @@ -41,33 +40,6 @@ impl AsByteSlice for RGB565Pixel { } } -/// Representation of a font. -pub struct Font { - /// The size of a single glyph, in pixels. - pub glyph_size: LcdSize, - - /// The bitmap data for the font. - pub data: &'static [u8], -} - -impl Font { - /// Returns the raw font data for `ch`. - /// - /// Each entry in the array corresponds to a row of pixels and is a bitmask indicating which - /// pixels to turn on. - fn glyph(&self, mut ch: char) -> &'static [u8] { - if !(' '..='~').contains(&ch) { - // TODO(jmmv): Would be nicer to draw an empty box, much like how unknown Unicode - // characters are typically displayed. - ch = '?'; - } - let height = self.glyph_size.height; - let offset = ((ch as usize) - (' ' as usize)) * height; - debug_assert!(offset < (self.data.len() + height)); - &self.data[offset..offset + height] - } -} - /// Primitives that an LCD must define. pub trait Lcd { /// The primitive type of the pixel data. @@ -186,26 +158,4 @@ mod tests { to_xy_size(xy(10, 20), xy(14, 26)) ); } - - #[test] - fn test_font_glyph_printable() { - let font = &FONT_5X8; - - let offset = (usize::from(b'a') - usize::from(b' ')) * 8; - let expected = &font.data[offset..offset + 8]; - - let data = font.glyph('a'); - assert_eq!(expected, data); - } - - #[test] - fn test_font_glyph_non_printable() { - let font = &FONT_5X8; - - let offset = (usize::from(b'?') - usize::from(b' ')) * 8; - let expected = &font.data[offset..offset + 8]; - - let data = font.glyph(char::from(30)); - assert_eq!(expected, data); - } } From a9e8ae6ad3cc8e8de6ef8ade78ea677b861490ea Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 16 Mar 2025 07:59:32 -0700 Subject: [PATCH 040/110] Parameterize the font used by st7735s Instead of assuming that we want the 5x8 font, inject it from main. This will allow the user to select a different font later on. --- cli/src/main.rs | 3 +++ st7735s/src/lib.rs | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 22094bdd..491edbb4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -212,10 +212,13 @@ fn setup_console( #[cfg(feature = "rpi")] fn setup_st7735s_console(signals_tx: Sender) -> io::Result>> { + use endbasic_std::gfx::lcd::fonts::FONT_5X8; + let console = endbasic_st7735s::new_console( endbasic_rpi::RppalPins::default(), endbasic_rpi::spi_bus_open, endbasic_terminal::TerminalConsole::from_stdio(signals_tx)?, + &FONT_5X8, )?; Ok(Rc::from(RefCell::from(console))) } diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index 5ac72b38..c7edaa6a 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -29,7 +29,7 @@ use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, }; -use endbasic_std::gfx::lcd::fonts::FONT_5X8; +use endbasic_std::gfx::lcd::fonts::Font; use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel}; use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiMode}; @@ -472,6 +472,7 @@ pub fn new_console( pins: P, new_spi: F, keyboard: K, + font: &'static Font, ) -> io::Result> where P: Pins + Send + 'static, @@ -482,7 +483,7 @@ where let pins = Arc::from(Mutex::from(pins)); let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; let input = ST7735SInput::new(pins, keyboard)?; - let lcd = BufferedLcd::new(lcd, &FONT_5X8); + let lcd = BufferedLcd::new(lcd, font); let inner = GraphicsConsole::new(input, lcd)?; Ok(ST7735SConsole { inner }) } From f723b975c08c0d82433b5270d4480e12dac7aca3 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 16 Mar 2025 07:52:34 -0700 Subject: [PATCH 041/110] Add runtime support for fonts in st7735s This makes the st7735s console driver recognize a ConsoleSpec and uses it to support dynamic font selection at startup. --- NEWS.md | 4 ++++ cli/src/main.rs | 17 +++++++++++------ cli/tests/cli/help.out.rpi.sdl | 4 +++- st7735s/src/lib.rs | 23 ++++++++++++++++++++--- std/src/console/mod.rs | 2 +- std/src/console/spec.rs | 7 +++++-- std/src/gfx/lcd/fonts/font_5x8.rs | 4 ++-- std/src/gfx/lcd/fonts/mod.rs | 16 +++++++++++++++- 8 files changed, 61 insertions(+), 16 deletions(-) diff --git a/NEWS.md b/NEWS.md index c12e204a..db00040a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -27,6 +27,10 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Added a new `endbasic-st7735s` crate to assimilate the driver for the ST7735S LCD, decoupling its implementation from the Linux-specific bits in the `endbasic-rpi` crate. + +* Added support for font selection to the `st7735s` console backend. Fonts + can be selected via the new console flag format, using a format like + `--console=st7735s:font=5x8`. * Fixed handling of extensions in the `LOAD` and `SAVE` commands such that paths like `LOCAL:` are flagged as invalid due to them not containing a leaf diff --git a/cli/src/main.rs b/cli/src/main.rs index 491edbb4..995855ca 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -81,7 +81,9 @@ fn help(name: &str, opts: &Options) { println!(" 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'"); } if cfg!(feature = "rpi") { - println!(" st7735s enables the ST7735S LCD console"); + println!(" st7735s[:SPEC] enables the ST7735S LCD console and configures it"); + println!(" with the settings in SPEC, which is of the form:"); + println!(" font=NAME"); } println!(" text enables the text-based console"); println!(); @@ -211,14 +213,16 @@ fn setup_console( } #[cfg(feature = "rpi")] - fn setup_st7735s_console(signals_tx: Sender) -> io::Result>> { - use endbasic_std::gfx::lcd::fonts::FONT_5X8; - + fn setup_st7735s_console( + signals_tx: Sender, + spec: &mut ConsoleSpec, + ) -> io::Result>> { let console = endbasic_st7735s::new_console( endbasic_rpi::RppalPins::default(), endbasic_rpi::spi_bus_open, endbasic_terminal::TerminalConsole::from_stdio(signals_tx)?, - &FONT_5X8, + spec, + &endbasic_std::gfx::lcd::fonts::all_fonts(), )?; Ok(Rc::from(RefCell::from(console))) } @@ -226,6 +230,7 @@ fn setup_console( #[cfg(not(feature = "rpi"))] pub fn setup_st7735s_console( _signals_tx: Sender, + _spec: &mut ConsoleSpec, ) -> io::Result>> { Err(io::Error::new(io::ErrorKind::InvalidInput, "ST7735S support not compiled in")) } @@ -233,7 +238,7 @@ fn setup_console( let mut console_spec = ConsoleSpec::init(console_spec.unwrap_or("text")); let console: Rc> = match console_spec.driver { "sdl" => setup_sdl_console(signals_tx, &mut console_spec)?, - "st7735s" => setup_st7735s_console(signals_tx)?, + "st7735s" => setup_st7735s_console(signals_tx, &mut console_spec)?, "text" => setup_text_console(signals_tx)?, driver => { return Err(io::Error::new( diff --git a/cli/tests/cli/help.out.rpi.sdl b/cli/tests/cli/help.out.rpi.sdl index f25a8636..aa511753 100644 --- a/cli/tests/cli/help.out.rpi.sdl +++ b/cli/tests/cli/help.out.rpi.sdl @@ -18,7 +18,9 @@ CONSOLE-SPEC can be one of the following: individual components of the SPEC can be omitted RESOLUTION can be one of 'fs' (for full screen), 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' - st7735s enables the ST7735S LCD console + st7735s[:SPEC] enables the ST7735S LCD console and configures it + with the settings in SPEC, which is of the form: + font=NAME text enables the text-based console Report bugs to: https://github.com/endbasic/endbasic/issues diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index c7edaa6a..edc0d04d 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -27,9 +27,10 @@ use async_channel::{Receiver, TryRecvError}; use async_trait::async_trait; use endbasic_std::console::graphics::InputOps; use endbasic_std::console::{ - CharsXY, ClearType, Console, GraphicsConsole, Key, PixelsXY, SizeInPixels, RGB, + CharsXY, ClearType, Console, ConsoleSpec, GraphicsConsole, Key, ParseError, PixelsXY, + SizeInPixels, RGB, }; -use endbasic_std::gfx::lcd::fonts::Font; +use endbasic_std::gfx::lcd::fonts::Fonts; use endbasic_std::gfx::lcd::{to_xy_size, BufferedLcd, Lcd, LcdSize, LcdXY, RGB565Pixel}; use endbasic_std::gpio::{Pin, PinMode, Pins}; use endbasic_std::spi::{SpiBus, SpiMode}; @@ -472,7 +473,8 @@ pub fn new_console( pins: P, new_spi: F, keyboard: K, - font: &'static Font, + spec: &mut ConsoleSpec, + fonts: &Fonts, ) -> io::Result> where P: Pins + Send + 'static, @@ -480,6 +482,21 @@ where B: SpiBus, K: InputOps, { + let font_name = spec.take_keyed_flag_str("font").unwrap_or("5x8"); + let font = match fonts.get(font_name) { + Some(font) => font, + None => { + let mut valid = fonts.keys().copied().collect::>(); + valid.sort(); + return Err(ParseError(format!( + "Unknown font: {}; valid names are: {}", + font_name, + valid.join(", ") + )) + .into()); + } + }; + let pins = Arc::from(Mutex::from(pins)); let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; let input = ST7735SInput::new(pins, keyboard)?; diff --git a/std/src/console/mod.rs b/std/src/console/mod.rs index 3cb0c9fa..8983fe49 100644 --- a/std/src/console/mod.rs +++ b/std/src/console/mod.rs @@ -42,7 +42,7 @@ pub(crate) use pager::Pager; mod readline; pub use readline::{read_line, read_line_secure}; mod spec; -pub use spec::{ConsoleSpec, Resolution}; +pub use spec::{ConsoleSpec, ParseError, Resolution}; mod trivial; pub use trivial::TrivialConsole; diff --git a/std/src/console/spec.rs b/std/src/console/spec.rs index 90e1f066..7741c30a 100644 --- a/std/src/console/spec.rs +++ b/std/src/console/spec.rs @@ -21,9 +21,10 @@ use std::io; use std::num::NonZeroU32; use std::str::FromStr; +/// An error while parsing a console specification. #[derive(Debug, thiserror::Error)] #[error("{}", .0)] -pub struct ParseError(String); +pub struct ParseError(pub String); impl From for io::Error { fn from(value: ParseError) -> Self { @@ -154,7 +155,9 @@ impl<'a> ConsoleSpec<'a> { self.flags.remove(flag) } - fn take_keyed_flag_str(&mut self, key: &str) -> Option<&str> { + /// Queries the value of the keyed `flag` from the specification, which may or may not be + /// present. The value is returned as a raw string. + pub fn take_keyed_flag_str(&mut self, key: &str) -> Option<&str> { self.keyed_flags.remove(key) } diff --git a/std/src/gfx/lcd/fonts/font_5x8.rs b/std/src/gfx/lcd/fonts/font_5x8.rs index 9dde31f3..8e127d0a 100644 --- a/std/src/gfx/lcd/fonts/font_5x8.rs +++ b/std/src/gfx/lcd/fonts/font_5x8.rs @@ -921,5 +921,5 @@ const DATA: &[u8] = &[ ]; /// Small font for tiny displays. -pub const FONT_5X8: Font = - Font { glyph_size: LcdSize { width: WIDTH, height: HEIGHT }, data: DATA }; +pub(crate) const FONT_5X8: Font = + Font { name: "5x8", glyph_size: LcdSize { width: WIDTH, height: HEIGHT }, data: DATA }; diff --git a/std/src/gfx/lcd/fonts/mod.rs b/std/src/gfx/lcd/fonts/mod.rs index 8be2447b..8a4576b7 100644 --- a/std/src/gfx/lcd/fonts/mod.rs +++ b/std/src/gfx/lcd/fonts/mod.rs @@ -16,12 +16,16 @@ //! Support for bitmap fonts directly rendered onto an LCD. use crate::gfx::lcd::LcdSize; +use std::collections::HashMap; mod font_5x8; -pub use font_5x8::FONT_5X8; +pub(crate) use font_5x8::FONT_5X8; /// Representation of a font. pub struct Font { + /// The name of the font. + name: &'static str, + /// The size of a single glyph, in pixels. pub glyph_size: LcdSize, @@ -47,6 +51,16 @@ impl Font { } } +/// Registry of all available fonts. +pub type Fonts = HashMap<&'static str, &'static Font>; + +/// Obtains a mapping of all available fonts. +pub fn all_fonts() -> Fonts { + let mut fonts = Fonts::default(); + fonts.insert(FONT_5X8.name, &FONT_5X8); + fonts +} + #[cfg(test)] mod tests { use super::*; From 0851e729dfcbe65303e06b9d5ea9ed87c9e6dcb5 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 16 Mar 2025 08:18:06 -0700 Subject: [PATCH 042/110] Add a 16x16 font for the LCD support This font is too big for the small resolution of the st7735s but it is usable in larger framebuffers. As part of this change, fix the font renderer to support characters whose rows are more than 1 byte wide or else this font doesn't work. --- .NEWS.md.swp | Bin 45056 -> 0 bytes NEWS.md | 3 +- std/src/gfx/lcd/buffered/mod.rs | 56 +++-- std/src/gfx/lcd/buffered/tests.rs | 98 +++++++- std/src/gfx/lcd/buffered/testutils.rs | 14 +- std/src/gfx/lcd/fonts/font_16x16.rs | 329 ++++++++++++++++++++++++++ std/src/gfx/lcd/fonts/font_5x8.rs | 8 +- std/src/gfx/lcd/fonts/mod.rs | 11 +- 8 files changed, 484 insertions(+), 35 deletions(-) delete mode 100644 .NEWS.md.swp create mode 100644 std/src/gfx/lcd/fonts/font_16x16.rs diff --git a/.NEWS.md.swp b/.NEWS.md.swp deleted file mode 100644 index ce8691023219d850e465df62fa2a9c666ac1b879..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45056 zcmeI5d2nP|b>VgQu zG2sYx#}_Io^WEj#bI(2NeQOif%^gp!7@w^0-)~hazyFKB+IacDy+h@zQ&(1QZnsbA z&%f-yM)oUxt@2bgc%Yjf$U2R+G;h`qEX-cNJl<~nvJUn>-=n}D1%5pW4EpQU%kNsb z_~7J(8oJ>82PgNs=jyN5=HAnL6xgG{9tHL&ut$MC3hYr}j{Ia9Px7qdcw&C}yZsmKo3x9vyuAi};+lRj|g`a;Oe#Xy#bJyp02p`1H?+`ve z5q>^)*XMT(f3Jm~Z`$?wox~p~z!>-({)G2|TR z^}4&j(vu%vp09XZ3D_f24WVh@^lT%}&BVPBmHB z?s}y%ZB%15G>%ivUVo6bl6G3(WMV9H+S$nR!#=_7Yz-oJ@@^|kZex)4Qidtock(8y z*@48_W(pO(LB_&wuu!Mb>}B=9Z_HkJo6w&0cQ!WVX@`M&>Nk?8SjYE88Lr9(HFp403Hb zbG^w#LI)ZSTV;z)+=q+UWG(n8PsSL@Qf+@yyJmL&1V2Tk z+Wg$|N^M`UWb4h-Gz7;rcxFSqp=X_D8U<3My%iUP!PJ|1#y%BWbs>$RVE3daQk|-R(Dg2qs(B(oQGq?N7XWEnU`V(js3)iu4D$4{j{yLX;;Wxfe3s6h^wG|#g(eIRpLNC|NGJq=^liK3wvGV$Qy#G5Yj)<&zlhI}c` zMx=MD9e+GozGiA^cBVG&BWz@CSgPNH0(laKu#;?edna8S)Q+w$Os~u>vRiChF0URD zQFL^)y7iOV?<)-2<;m>v#pT0C=Z-8cOifSEjUSm>t|jU~yHjWUuQ2X&oP+sbt%n?C z>Pd&Cr{~(WbMy1HWZRBgTNDV{qeIpLQX&D?o9G2ex-LGc_cCL!W*nsl1+3av^HXEoBG-qMO84?dna95wr)aJ@TVH!_?i zN7b(La2>&Dd^O+f3{F?`t*nkvu50saST=NLpVU{e=(IR(tYc9CzHGLo!TBC(^kDj4 zJdB3pmTs8TI4ZA_+L4v^YB(@!)oM5|u7WMLHkU0#o z!>8e9n=-6-%*QQtNSi?#ExL*`c$D>Os;nvp)ui5q%}i{#s6ZQ|5&p*Ncanpr!_F)Y zP0=L^LzlFOh^02NhKIqruKd*T}g92+360XxFpCH2EI0Ewi+By zD0rMQ9FL~^BCZkiM({+u$hp?b>>CyB(zJCl>{dj5cff(<3N4E^H|#)bBi+!}VLh#9 z;SfX&4_Xb|=4i-BC)!7?LSXp$5B4~6K6-P;X|B|}4MlUo{@?PWXmG@GRX zgqS)pw~vWl7mkh2ppAOyqflGPY^Q-#o|}en9mb88Z8k^KY_d*JT4_YagKZCZ)06`j zK1nir8UfNDCCrLVs6OcRIN4@QaI-Cg{ z!9fZ~U8p!eIJo5*8As*0Xvt4sIm42J69*@%6Njn?59;{3b+ure5)Y|v*g084Cxa`t zd9#sKv-Ndzx}Xo}xIhzVp=M%N);GHxN+<-b!L-wpDEC-)Jj77ZF|0|`ucS0_wwu;2 zH^z2HirN6@RPg(q%(5-FNz1C(dJlhW0RK?K);QNS9uEiJ#4*8hp+-_c?Vm z5~+I92+UigvEi56z#6f66~x*^!{rEe=O$xn$R@?*1fgBsH8)6@6kb9ci*nCdyKeNX zb@Ak(j|M`@rNF;)T%mZ#IOFl1&{oNOFftuq#JuUE^hRf+?Ce3X7qHA2XoH3r5lxHm zYPQil)s&e8GeLAy?NH_eCDRsWTSFckbDW@@*806cc3>R`9;Ss^7?>5N!;elXxb~9T zPL?0&bi?B99O&p@J`t&x^`2I@ru(>&~R$?=(9BWk*~^nk$EN| ze+%W2CE%f>a{L7)5~Wtkl1LZTA7@#ZiocgRQ|l193YV+qTXHB=tD9MCD@?Ik4GJYr z6s%de+_phv)N1gYF&rtr3<*OQecNDr8{5#sHbgbFIHG@|F zaAia-b~lpdv)=3t6?0~Y>KcfohdX$f%?TsNqYcQXw8E3!P89>^WON$HQ`p=Zgt$Rr z6-A%nZi_lFk;qkBT3x`;AyTQ`9KRjx6MPZ3*6i%e+=_gW=6~cT^`H86X?9`tcx^by z+AjYPB;IZk`-skQ^C=}`h5v;eTnxnhoN+m{I*jQQtmO+{IFn>Y2aiQ1?QNLjOb(Yt zi@U&#eX~Rme63b@o25zC@o*q!+)9*zoJM!mVT`S%*%R}!4sOKAlBvuX@!XmaVn<`z zZ=t*s!BI=K=mx;Of*(<$4a=jl5*&qRuCRFRrCgV*TbYSmj^_~)gr2&+0W!;_23of?B`CicEfh{Q_8;to*GY$sZMu=MYn2J0_5BnOv}l_Tsycp_kQ+Y{QeYpqojwF>?~*P%nu# zclZZh+2y{<_u+;>-s9ixam1VBF-XcF1l{dco4tW;tGSERgvDwTCWMWCr{(;`OB1%B zKS9v2zlj4G^E;kbM!R~tr7e^w3boF*;z`0&7LDWig)X)$>$)~zv*EB-^lK=J#e}_V z4GLp&mz@it=jdk}(ga>WE^cO7-yO7yfF$!%3&)IeS(B2CpP8L6{Dqn+hYF{<&KZJe zgh0!xqS3TT{cE} z77pVT_atcnY9>?T=URG}$&ddiDb@$ARyD^%D}OzH(-3A+`)2>Y5Ignd*t3fN$Nc_Z zVC#Pk`~&z9_#k*Xcp6v(DVPN_;2@BH;H!7z{s7PcZ7>Ed1fQ494?YT>0v!ARZ+ zZm^JScJR=phZFaoG`a&hLW+b~k4J>GjO%gpceiu-PCOy_OfMY$Us0VrbWSfE{d4J` z;{ORs-fPK0m;E2>_uq#-e+%e?3&5wbk&@KtR5uYebUd2lWGIrjdyz&F8rz~6u#mo z7d!`C1?~*)1l~iwL=V)#8n^}=13x>jQh5{jBk+e{2K;aG{J#Uf4PFWA;E~`e@Lgyw zy1z!WKgWM^9};gO4bXxM@SQw6xcG5MTE4GknXl# zEhi7LM6R2ZNB9t2Z5lqDOxmL*{^6X8{`TnRq-nXy`kSQuDi_p;A`iX!|E5dHvb(a! ztnU%WU7xsRk~U6}{4>mRPikaK`fz<~g?$La_P@FUZxa`#)!wI0dfG2|8s7z!`2&%=LJC*Ps6rdg$dDxbeLwJ;;UE9rCkG4#FOz(2JsLb9Nq z-&X#57=sc>w>Ps+p!&nsZkn8!oKO=FYuq$>@Zv-E&+bi^U7|l~V6}&x=7aSR{BqB4 zU`x939MDomH!Dk~{yI5rgcvNLdp~hz3n;m%dh(z_=Y_Y!V3)Pn^HC-bs?~-1~#Fi`z`%5&EoJXj&UL@R~nvmMuQHG?P zek)1~>_$3czp7MCjtrZVB1Xx~V$CYfh;12MXUzb-Bj z(_LOYKASAe-mp?5v^(w5E9#R}zD7JW&pBek;)aw?m_-AzL=sfSk+b38CS-0?Lu8vQViV zTeLz0YjkOLWp!x*$>v!6+wp#_oNUdrM?G3;+f&mexTy*(v7&|c zUCQed6cv@V8Slh|Sq7<(6vKt8D`e7;JJY66Ogz1gY(%#hd7`2v38@7IgBeBq7q#GMANx?>3)`grPk;oc*%%E96dBl=+8{J{j6K4dOB)JL>tpY*AL^64Z zl4;Z?Clakl!U-|5w3l3RwxvCPWIRYgid(W)e*XUCW+f>pHz|lu9V@ofqw?{Ri_g}q zw~~yN@@1e71(B-aQu+|$xJUF> zD4VUl+oU>Jojh1QcL)dTC9dT84TJN1BQeD~pv+5i6RdL7*56h~{gx5E-iIAsGkVh^Z8%yO7x0R@D+)BGweU z1`MZCGXal~VbDi2kg-80sy?+KZYD9IV=7OI{#VGUhSxPtDu& z4E=OqWnyMSNFRyixk2p+>Uz*_s#XA3(n%3m7a_bZD)iQ&iZ$3myi=9~A%@~Z%%!%X z)?#(yxv50{+!uTV`~LOdb>Io$UxO>bVelPn{MUddf-Uec@KEpt?EF6f zj|Yzf_W(b`&i^TR3sC<4Jh&E|2Yv(mH8y?=JP7;(+y4K8w}Bei2gbmcu<2g_9t~8> z{}F8X4}-UWH-i^|-v_Ge|1oy_8^II6A#gExAb0@y*m>kHfEG9j9t1vx9seBgY;YKS z3p&0GJO_xT&jP=~f7%aTP8UvbiB$bD6t#uj6-r*D2U;d4Yu-lRmf2@Ew=``mb4uqV zI${~k7S5z*-I^=sP35o%%r9{+nSw}1HpqU&@YcVjlZ0YkXK%5XWd@4bjf+RMI9S$m zu}V`>+Ec5$LD@3}bSU&nTnv(^W)(z-4;-jD7i-VL@d?VLODEbZvURonL#g1@laxX< zH6sfc!%Y^j8w(s_!LGlCI(Rzd=$D(!B(8ejy+=05YD-(x-%1iFIfe;tbkkO-m~so# z=V)_Zo;$X%dSb-gV0DiW(GCS>uhl?*pd^^v17dZcas$VQTQztcqLPHDmJVWbmoit( zPf0?fb2Q2^C&{K_UTpFdW29#*vbxnkH%FTYRoK#dl)qi5!g>_Z6ANE8ui03)G*Z%a z>nEKj6c^|9$a!e8Mj498C=4I8U|4)w zNOemWi-!{II_Q*0L&G}PBoW~@R@GTv$2ZP0KDE8&e3xgHk$FbdSLcEGx+sY2n|y>) zyM`qpU+R`gqW9?hXsQ6-c*n`Y8bu9=e=@45fVb_Ak3g5mlW_sgs`gA=TAjG$oSxh( zqSg~sWb8zh*(rnHwKfcuQ2WL%+5}nvJEoimhYL|;x@pYXj2zNd7}sJ`K%ik^XOt82 zo!A0NT1|(Zh`*@V!=9jkqNuX`nplCl0u0z#Nj=4uOm& znoM5~id0O^^^)V6sg?Dgy1;O!wT47RB*Kzo?$PRUdN9vr+ znV>ohuOU&h(g7(a@YA34cjGq^l%mbMPYHS{pJuc5ofIRhduHahKjS&85X1Cxb4DSeqYv7>2yPU>lBNSPNPMYsK%p2TFlEM> z-z7WERV#L~D2+0lD1K>0knaU?ZT-V>R68+;g&mD5_?(0e-(ET|l~6mjPYwpNq0C^Q zyRzMRM3$;8{`8r?aCWMEz{QNJRONFf#A|NoINX|pOZnQ_8_k>G{A_G5Up}`ErAWPa zQA6p-J25psyRtGnylxQg_doUx*yI^r9G*1EVpj%5*FliQxu+gauPn_! zd|IwHTGF>U5u%wmtWfa64YwbncgabjO<3H_6g))`Mz_JBdlXgaR;FB=$;hC_XG_U} zR)S^W8z;HiVq_YHYr&ip>Hta(+6Z%^OLoJQC^G8lk_eW7U<;6bOG1VlUi4I4Q)Pyb z3AT~!Z7o3U*wGtqnx0#tx+A!>hJQF(XjNJ~wAqwnq1j_@>>(psE(cNY5rm5k&})M5 zb1H>T6+)2HDGEl-t?2)n*|}ra#9}k`fBoFd$~6`tz!{(-cv!bgsENy?560S9p2jtJ zJ$G-Y)f&H%=*@4%cGKvKhBynK6gGkvx#`Xjiaw;jBhzTmB!L7;$;)KBeJG}*B+;N; zPQ`+`l0?zz#T`8g2^Nl1pEofz6U_t{d0`mWyHbF##} z54u4|UuOPX=;K#}Fk|;}l)P%{n|Kx%e3Z@$yl+YByQw5%rk1A_W0}6DNS9w;DMd8m z`lD!|_G=B?7_UBYK#)Mk^KiSW?A} ze)ZkZraY{Prpi}UYN^hPgF~_|UIoUMI7juP+y6%~P+u=QmH2 zbHQ`K<>1@c_%8#C;NIX~;1k&PPXo(f2~2@Uf$wA6e+YaKoCagy%h>j++yAfN-QZnd z9o!du7`y&2!C!zs1}_Cq1y2E0@KtR6KLWF$3ig99Vdp;&oCG(62Y`=b>pvSj3pBwd zmFY$-U;jV0zG;r8g$$)1*Z*PJmjy&=Uxm{?N+!70^b592>2eG7#E>;m zXfB><=xi3cDy(QWqr_0D7m3jMxrJG6Vg38W+zm*F@XHcGrt-g!@{3s2zPWtk!gS5M zk&I~e>~=1k^ufDvl!BEb%iO^;ugi6G`&WH&*`E(7FfR2&U0!K^2&8Cl6%#<2ladr~ zv*Docv*~m*BI9viAVJ{aCzXK@`A)3~tYuc1l1acvpvM`{@5;gcqA z6>CtWpl+{h`*eQi?MBNbJ_lq<0&V_weP!(W1ekWV=v>sv>H~>yK zP3a-smaHx&=#G_OPpOVi?gvaz23LxQ8LI{FTBYLd9H>PorAh$Pbf*m{#b#j|qI4IB zkX=cbXE=-)m^qokx|qTfJki)+fiZoZ{VTL$1gb>FiOU58i~MPh7 ztWpe0p4quBE4Vy5FN_!HFby{giofu34w;K<0Y@A+D$7=}t_=!uyEJHXeG&H&c&WAd z4Pb#>3#da?=38B5zTvhaEk1Ya=;~1g66szRJ@wPvEg)Gi5?z)}&3KM=yTmY(DNOAh zwH6tBq$tss4}z~OO-)}ryR_d0oh_Cr++*^tgl&cGA2b(6U{@;2At(-pPq}VQmmVQ2 zJnKuoN)tTM^P&ml=no7&@I|}XXb@%51ftb)2rbr?M>ADI4Y7?x_N(nk+XX~+wNDT< z+EXMNYS)s~v9akh$hqK;&$}YR|%qZu?FD;+GTzQ+Cmnc!UyCr-HhPA>u_iTFYvN(uKHaYi( z71348f70@DMS(0h4kexNoWUD*39z-U&!fhRKrla?$>noh0%jCc6x0NVt^v^~M~+p= zkKsst{Bf`do66L#OFUXaa!>TkF=x4roG3BW8McO2V|7lh zD--*$Bg~-6jR*yrmmMI3ksoDFjW}08q?UdDrg^CEjj(PZv!*PBjv6rUF@pPJb)csOsgwL ztCzd>7FqaGJQ-0YD1Ad7ye=6$T$)0rkLMhR?iInXzI`!@dg8JdU_F#Z%IP7))?f7T4Fqc|+s7?mmrj-FU9Jn^81~A)l~5j4h1N zF2Y;Sl$ly14`7pPVzw1+AsM8`sl{GtXEkdtsGO3C^4l3{eNQmhDdagOI){`kv$Ttv z5sx5UZ03NqU)s@8)7IW4CAF%l!+dDZDAz>@%-0}v=;T)352?iJYbei3bSM7vOJ-yo z8AxrjOOw^5`O)!j_WN5o zt4`Lb8$=WYkQNc6bA=VJO9vI))zM|dcTKw#&*wz(YkY<#iWW&NE$Z^5z#Cdc=}>10 z!r*^3|B!D>kFi?(|6gIBc4enx|DWK8{(CnzzheLQ54Qh1u=(E(YT%yWBiQ@8>;EEf zcW^iGUTpm*fkyzv`epYk&VLFFz~jN=z`enTu=Ae;9u4Ne&#~*@4xSF42ByIg@K4zG zZv(FauLREozYBDazheHs4OGwn8K4~h7lJA9Lu~xlgO`981Ni`+39ba+!q$H|XoD6= zz-O@cp9`8`6U>8a!2vK1zI+~c5rC_~UBO+z53%)s0A33o3+@VTfsUeO=j?VrqS0fK zR~qlKdUiQ(bgB=frQgvhWl%2iu?)^Y65(;TyE}`!w~gy@I$4Gm)*9DwS9OT6_bnmd zN;&IvaWICr+Y%fcx?dNO4EHD~H+9sjC1ILRdGn!d{%6!^>~T zo~MhY8;5-$(^6~9(hWw$$;{H+b+d;LoS2f&b$AMU_5!YD-Gt*(+jT{{I*Y84yDZYi z$HZ`_*t*tXp|yz9y&k{?n?w}usdA#T?n3ew*r6kSY`$y}RMBaBQvtT&^-Ub{u3;2i z#jAfgytw&=;km#ojtHeRYGynTHo9FOtusNly_+AsQnt@V?p9sV&C9$QfJH+EaqAIL zF*MXty~e;UcJj&Km*gZfZA^VX`o*j*6D6EsJ zbeg#MoF?97(Zq|q70p%aQtlomb*}vcS^h$@`d9OQq_SHM<>QYpoZ(4X|6bfVqhAY z`yqFtS-6B?4xTfEx|r7)w^7>-L9U#X^gb=ona|q&%i)2zjf%Gz7+H$y3|%gSC8^7i zL#hnwz^=H-S^GfgAYC_Y)r>St$OpXyAZS{{X_PDw23jQep2-2+l6`CXyf(94M(!(0 zo)XR)+<10*)1c!w^8|&)cUQEoYRgl0|2L0#g0xmO+@WiYnVu8Btq)yuwq&RfTV#t2 zW3~ut-0yfb?J6Yi%x*6;DvX4oqg*OP1M_!7NO85u7-2B29Xv+l@)da(oTy3kuSU&E z;zVO4{Svv$=0#4(h<1BldcW)H+?>aa&g0(1aay&fjubY99+gpRkGkWFEvHKZITCEM z2jyclqU{Qa)#bzwG{$H}OkQzo!3>RezXrQMIiXW*b;}87zs$(9ppk^-H$B=ap-RET zpfv?gEK{T3#G~o8WeT;zLU4t2DFVk)uwwVqYv*^lou2$Qd-Z~LzXzyGL>6H=BdGuo zd1_0FvT&eobUVsuPzgz~ka0`ZrdHH7S3GgkQ+dIxEegntQ~0TiC?kaiOj0~;` z4Z4K+;eyjh1Phj|9+L~w(-AaUUD^j_xC2haP}!H7#JQ3MD^O1#J2AItM09(%UrHno z1mP&eFgJ=67X})OaN_wTj0i$C6{%@tnT>U2`XtWY)=+mAMiLF(W_)eO#}?0ZVgMPQ z7mGuC24WCZfvJ^)94=Lkv8)Fkev{wrI=H3MIU#!hO8JfkywCUd;Dfz$Yaib<)n)KF^5Fo z@$#B-1()V&9Lg~roOe18r8%7?Cgj2Z?h0h}DKQ9}Z1E`$qcC3iBw7UB%qbzkJ7($; z@>xHX>l#JXzPJrc{pw`8qEIPb1{K&c#HphmqUVV1bp?DNWkhBi^W6OA=VtxAF2WnV}9MJ97}3X z?>lsPwi11t{f|jp`4icz z*#9+t=)cck_kSAvE~tYwa9?m2aA$A}K7by02>2ql{|mvh!Lz_2@OA9}4}uSXKL#%a z*Mc!{A^1E#0KEtBbmb6`$K18RNY|he5bwECFN^)F)nHcDUCb%e&07m`eI1~ zkt*lBBvu^O9!aez&dV};ags|ZmzrX)d?Le|D3Ny%+qHzmLdFx^30CZQPCXjQx3 z>RS=GdOW`5(Ik<)0?MYnUYZqI;bnC-4oL$sPox!mil`_p&NyhOw{DN@=LGWy>-~W%6u)2PdWSC z7_3*hKn*<+Ch7wS>b#w}@x+YpSsZkpsQVb~y?T-P(dw*tpA*~<8MRt`sN+3cqV99Y z*oi8NR!-Lr#}imvS68yzdpse!k+wJ-cPI5p-^HJ<%jLsM62l!E{=6C=4nqoyZ$Wc> zh@_z=7k?2i>-B{3BsR7hw8OguN3R(VV-I=- zY`&sA^AgKyt!m5jv$LaBJ6l$8P_nPs0b~-B^o>=c&ED+V#*RmvvKC!|$#$4894D%lgkY1)+)H=>%D`e8j3o*IAOu)$f zPmHhOQpI>+b!#Xn4uyx-#Z8D^wlQlvrqE!pn1!sDtVY4j#&=JfHB{5Sy6O6*xl(4R z?^mO%ed>9M@p)SUy&Eo`Mf6Zw4hz0SQWl%sQl!HKNDWh;%ao<-ZI%9;8=o9v5+Imq zqc@F2L4sX1LP8w9_Fl@vBOQ+ImUMcttIgUgQEm^y^g6-kTu z^rAE?L-WW_lJZhWi)pv>tVK9w_Ze5Smq?DtKNX>iY*24%dfrk?tfGxe5J*n3${mR4 zg39_Vw;4RlF_2sHlF@jWgep)ZX}q~c%;Ql6QNgl@y=A^*)utwF)D4G)ym+I45%gXaE81q}_Qf^+Mv8FEqvkj(k9q*@rNbp&cD|rM zEW8sJF-7JCYp!)I!n6Frn|1M`&w}IM0=ZhSGxp6ihe4?$cN$u=@loR@n}Hgvow$DF z3GWbb8wMknSV(qbzmCL}NjlX1f0(nC_sQPG{%^|cM!9QX9zX9lOfE&Rim;hhL_Wx_}SKzJS zPr*$<_W>RT-@^WXCHObs$>1jN1aNQg5p4bEfqw_C26qN`0^h~X{|@-1?0xV~@D8v6 z^ge(`fbU}We-V5Ed;q*3JQX|zTn;V+dOyJDfcyim0j~zvfy;sX1mA;zuK_mz`3L?p zw*RxhGr zKT57!3&%)+`MU zcO_K%TT6Ky)1tqPl;vWUaI|ydniq`j~P#B;tNd_e9DUtab3Bf& zROq?1vV^Z{WMF7aOt8=aIEsr2sMYz|qyDP0S&Q|~TLTK`&JoQm4=T=3(^DO`ac-b7!zNy1Wz0j$o(Yaci2B!>23bqY5cuJSzbyoquOLD!4f8+&5?(iWy1+ z3Vv*78@I}@B<{w{@s!v?y|pBp5rR^=W7q8P21QN@wL|2xZEwXao5Quy#2C-F(i2Y2 z!1OMgvXHG>wP%oTiipErY?$3n5VxBP|Nbx!$KD#op2D2y=voQ(VQ9$ODfq@tTLb{- zuW`v)-Z7@J?e146Hu4vVT%2T1n!{=yH-=qzVvWnGte0O8&oBwL>3*;E@M%j#BG5^d zI$6JR6`W2b(0p2W<`kl%BTv5>Mg`^M|ND*pkO18(51EbY@#WpNCN9<7^4Q$?Lr)_1DTq7OR$C zk2s93?7E&Z#_VW4*84zyWmhpF4Z~bGZaztnpLuAKzqO^rGxsOR-n2Qn2IY3>+4dd; z(Kvm`9FLqJ#@{`ssP_DUQgb#MpS8wDUocW(^jb=b#O z!iRlpykK^$8LJG;h%U~cLPRY=O#*DFf@nf;)fC*u7D4c+j0~Eaa2t{0!Dmb75?o@Q z+SKaG;_MBx(?fYE&zeor7*4TucQZ?npF^@0yGY_9IM70%S$VHX!%k< zVk&7<9}r!zGM%d5`e;?Js$hkH$Jw1xlv)8 zdaF%i=XkS+s%S?w<-!+VN)#%jCCZ2LB8zBejheO%UG4#GTCLix4)5HD%O$a1%VZRe z4J3+PtHA=;N}2c!v0Dxr2{pojrCJj!1`w0FJS>z7*Exg2l~nBoWt?|_!_S^0kx(3`3N diff --git a/NEWS.md b/NEWS.md index db00040a..0ef20680 100644 --- a/NEWS.md +++ b/NEWS.md @@ -30,7 +30,8 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Added support for font selection to the `st7735s` console backend. Fonts can be selected via the new console flag format, using a format like - `--console=st7735s:font=5x8`. + `--console=st7735s:font=5x8`. There is support for two fonts now: the + previous `5x8` and a new `16x16`. * Fixed handling of extensions in the `LOAD` and `SAVE` commands such that paths like `LOCAL:` are flagged as invalid due to them not containing a leaf diff --git a/std/src/gfx/lcd/buffered/mod.rs b/std/src/gfx/lcd/buffered/mod.rs index 4c7f5b1b..52de314c 100644 --- a/std/src/gfx/lcd/buffered/mod.rs +++ b/std/src/gfx/lcd/buffered/mod.rs @@ -326,6 +326,37 @@ where Ok(()) } + + /// Writes a single character `ch` at `pos`. + fn write_char(&mut self, pos: LcdXY, ch: char) -> io::Result<()> { + let glyph = self.font.glyph(ch); + for j in 0..self.font.glyph_size.height { + for k in 0..self.font.stride { + let row = glyph[j * self.font.stride + k]; + let mut mask = 0x80; + for i in 0..self.font.glyph_size.width { + let bit = row & mask; + if bit != 0 { + let x = pos.x + i + k * 8; + if x >= self.size_pixels.width { + continue; + } + + let y = pos.y + j; + if y >= self.size_pixels.height { + continue; + } + + let xy = LcdXY { x, y }; + // TODO(jmmv): This is very inefficent on a pixel basis. + self.fill(xy, xy)?; + } + mask >>= 1; + } + } + } + Ok(()) + } } impl Drop for BufferedLcd @@ -452,30 +483,7 @@ where self.without_sync(|self2| { let mut pos = x1y1; for ch in text.chars() { - let glyph = self2.font.glyph(ch); - debug_assert_eq!(self2.font.glyph_size.height, glyph.len()); - for (j, row) in glyph.iter().enumerate() { - let mut mask = 0x80; - for i in 0..self2.font.glyph_size.width { - let bit = row & mask; - if bit != 0 { - let x = pos.x + i; - if x >= self2.size_pixels.width { - continue; - } - - let y = pos.y + j; - if y >= self2.size_pixels.height { - continue; - } - - let xy = LcdXY { x, y }; - self2.fill(xy, xy)?; - } - mask >>= 1; - } - } - + self2.write_char(pos, ch)?; pos.x += self2.font.glyph_size.width; } Ok(()) diff --git a/std/src/gfx/lcd/buffered/tests.rs b/std/src/gfx/lcd/buffered/tests.rs index 8ae9b234..18899313 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_5X8; +use crate::gfx::lcd::fonts::{FONT_16X16, FONT_5X8}; #[test] fn test_new_does_nothing() { @@ -296,7 +296,7 @@ fn test_put_pixels_no_sync() { #[test] fn test_write_text_sync() { - Tester::new(size(20, 30)) + Tester::with_font(size(20, 30), &FONT_5X8) .op(|l| l.set_draw_color((250, 251, 252))) .op(|l| l.write_text(PixelsXY::new(0, 0), "#").unwrap()) .expect_pixel(xy(2, 0), (250, 251, 252)) @@ -325,7 +325,7 @@ fn test_write_text_sync() { #[test] fn test_write_text_no_sync() { - Tester::new(size(20, 30)) + Tester::with_font(size(20, 30), &FONT_5X8) .op(|l| { l.set_sync(false); l.set_draw_color((250, 251, 252)); @@ -363,7 +363,7 @@ fn test_write_text_no_sync() { #[test] fn test_write_text_clip() { - Tester::new(size(20, 30)) + Tester::with_font(size(20, 30), &FONT_5X8) .op(|l| { l.set_sync(false); l.set_draw_color((250, 251, 252)); @@ -379,6 +379,96 @@ fn test_write_text_clip() { .check(); } +#[test] +fn test_write_text_wide_font() { + Tester::with_font(size(20, 30), &FONT_16X16) + .op(|l| { + l.set_sync(false); + l.set_draw_color((250, 251, 252)); + l.write_text(PixelsXY::new(2, 3), "Hi").unwrap() + }) + .expect_damage(xy(5, 5), xy(13, 16)) + .expect_pixel(xy(5, 5), (250, 251, 252)) + .expect_pixel(xy(6, 5), (250, 251, 252)) + .expect_pixel(xy(7, 5), (250, 251, 252)) + .expect_pixel(xy(11, 5), (250, 251, 252)) + .expect_pixel(xy(12, 5), (250, 251, 252)) + .expect_pixel(xy(13, 5), (250, 251, 252)) + .expect_pixel(xy(5, 6), (250, 251, 252)) + .expect_pixel(xy(6, 6), (250, 251, 252)) + .expect_pixel(xy(7, 6), (250, 251, 252)) + .expect_pixel(xy(11, 6), (250, 251, 252)) + .expect_pixel(xy(12, 6), (250, 251, 252)) + .expect_pixel(xy(13, 6), (250, 251, 252)) + .expect_pixel(xy(5, 7), (250, 251, 252)) + .expect_pixel(xy(6, 7), (250, 251, 252)) + .expect_pixel(xy(7, 7), (250, 251, 252)) + .expect_pixel(xy(11, 7), (250, 251, 252)) + .expect_pixel(xy(12, 7), (250, 251, 252)) + .expect_pixel(xy(13, 7), (250, 251, 252)) + .expect_pixel(xy(5, 8), (250, 251, 252)) + .expect_pixel(xy(6, 8), (250, 251, 252)) + .expect_pixel(xy(7, 8), (250, 251, 252)) + .expect_pixel(xy(11, 8), (250, 251, 252)) + .expect_pixel(xy(12, 8), (250, 251, 252)) + .expect_pixel(xy(13, 8), (250, 251, 252)) + .expect_pixel(xy(5, 9), (250, 251, 252)) + .expect_pixel(xy(6, 9), (250, 251, 252)) + .expect_pixel(xy(7, 9), (250, 251, 252)) + .expect_pixel(xy(11, 9), (250, 251, 252)) + .expect_pixel(xy(12, 9), (250, 251, 252)) + .expect_pixel(xy(13, 9), (250, 251, 252)) + .expect_pixel(xy(5, 10), (250, 251, 252)) + .expect_pixel(xy(6, 10), (250, 251, 252)) + .expect_pixel(xy(7, 10), (250, 251, 252)) + .expect_pixel(xy(8, 10), (250, 251, 252)) + .expect_pixel(xy(9, 10), (250, 251, 252)) + .expect_pixel(xy(10, 10), (250, 251, 252)) + .expect_pixel(xy(11, 10), (250, 251, 252)) + .expect_pixel(xy(12, 10), (250, 251, 252)) + .expect_pixel(xy(13, 10), (250, 251, 252)) + .expect_pixel(xy(5, 11), (250, 251, 252)) + .expect_pixel(xy(6, 11), (250, 251, 252)) + .expect_pixel(xy(7, 11), (250, 251, 252)) + .expect_pixel(xy(8, 11), (250, 251, 252)) + .expect_pixel(xy(9, 11), (250, 251, 252)) + .expect_pixel(xy(10, 11), (250, 251, 252)) + .expect_pixel(xy(11, 11), (250, 251, 252)) + .expect_pixel(xy(12, 11), (250, 251, 252)) + .expect_pixel(xy(13, 11), (250, 251, 252)) + .expect_pixel(xy(5, 12), (250, 251, 252)) + .expect_pixel(xy(6, 12), (250, 251, 252)) + .expect_pixel(xy(7, 12), (250, 251, 252)) + .expect_pixel(xy(11, 12), (250, 251, 252)) + .expect_pixel(xy(12, 12), (250, 251, 252)) + .expect_pixel(xy(13, 12), (250, 251, 252)) + .expect_pixel(xy(5, 13), (250, 251, 252)) + .expect_pixel(xy(6, 13), (250, 251, 252)) + .expect_pixel(xy(7, 13), (250, 251, 252)) + .expect_pixel(xy(11, 13), (250, 251, 252)) + .expect_pixel(xy(12, 13), (250, 251, 252)) + .expect_pixel(xy(13, 13), (250, 251, 252)) + .expect_pixel(xy(5, 14), (250, 251, 252)) + .expect_pixel(xy(6, 14), (250, 251, 252)) + .expect_pixel(xy(7, 14), (250, 251, 252)) + .expect_pixel(xy(11, 14), (250, 251, 252)) + .expect_pixel(xy(12, 14), (250, 251, 252)) + .expect_pixel(xy(13, 14), (250, 251, 252)) + .expect_pixel(xy(5, 15), (250, 251, 252)) + .expect_pixel(xy(6, 15), (250, 251, 252)) + .expect_pixel(xy(7, 15), (250, 251, 252)) + .expect_pixel(xy(11, 15), (250, 251, 252)) + .expect_pixel(xy(12, 15), (250, 251, 252)) + .expect_pixel(xy(13, 15), (250, 251, 252)) + .expect_pixel(xy(5, 16), (250, 251, 252)) + .expect_pixel(xy(6, 16), (250, 251, 252)) + .expect_pixel(xy(7, 16), (250, 251, 252)) + .expect_pixel(xy(11, 16), (250, 251, 252)) + .expect_pixel(xy(12, 16), (250, 251, 252)) + .expect_pixel(xy(13, 16), (250, 251, 252)) + .check(); +} + #[test] fn test_draw_circle_sync() { Tester::new(size(20, 30)) diff --git a/std/src/gfx/lcd/buffered/testutils.rs b/std/src/gfx/lcd/buffered/testutils.rs index e40308a5..d3306aa1 100644 --- a/std/src/gfx/lcd/buffered/testutils.rs +++ b/std/src/gfx/lcd/buffered/testutils.rs @@ -16,7 +16,7 @@ //! Utilities to implement tests for the `BufferedLcd`. use crate::console::RGB; -use crate::gfx::lcd::fonts::FONT_5X8; +use crate::gfx::lcd::fonts::Font; use crate::gfx::lcd::{AsByteSlice, BufferedLcd, Lcd, LcdSize, LcdXY}; use std::io; @@ -77,6 +77,10 @@ impl Lcd for LcdRecorder { } } +/// A font with no data. +const FONT_ZERO: Font = + Font { name: "zero", glyph_size: LcdSize { width: 8, height: 8 }, stride: 1, data: &[] }; + /// Builder pattern to define and execute `BufferedLcd` tests. #[must_use] pub(super) struct Tester { @@ -90,10 +94,16 @@ pub(super) struct Tester { impl Tester { /// Creates a new tester backed by a mock `LcdRecorder` of the given `size`. pub(super) fn new(size: LcdSize) -> Self { + Self::with_font(size, &FONT_ZERO) + } + + /// Creates a new tester backed by a mock `LcdRecorder` of the given `size` and using the + /// font in `font`. + pub(super) fn with_font(size: LcdSize, font: &'static Font) -> Self { let fb_size = size.width * size.height * 3; Self { size, - buffered: BufferedLcd::new(LcdRecorder::new(size), &FONT_5X8), + buffered: BufferedLcd::new(LcdRecorder::new(size), font), exp_fb: vec![0; fb_size], exp_damage: None, exp_ops: vec![], diff --git a/std/src/gfx/lcd/fonts/font_16x16.rs b/std/src/gfx/lcd/fonts/font_16x16.rs new file mode 100644 index 00000000..9d306fb4 --- /dev/null +++ b/std/src/gfx/lcd/fonts/font_16x16.rs @@ -0,0 +1,329 @@ +// EndBASIC +// Copyright 2025 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. + +/* + * Font data taken from https://raw.githubusercontent.com/PinguinoIDE/pinguino-libraries/refs/heads/master/p8/include/pinguino/libraries/fonts/font16x16.h + * + * Original copyright: + * BigFont.c (C) 2010 by Henning Karlsen + */ + +//! Square 16x16 font. + +use crate::gfx::lcd::fonts::Font; +use crate::gfx::lcd::LcdSize; + +/// Width of the font glyphs in pixels. +const WIDTH: usize = 16; + +/// Height of the font glyphs in pixels. +const HEIGHT: usize = 16; + +/// Raw font data table for ASCII characters. Use `glyph` to access. +const DATA: &[u8] = &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x0F, 0x80, 0x0F, 0x80, 0x0F, 0x80, 0x0F, 0x80, 0x0F, 0x80, + 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, + 0x00, // ! + 0x00, 0x00, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x06, 0x30, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // " + 0x00, 0x00, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x0C, 0x30, + 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x00, + 0x00, // # + 0x00, 0x00, 0x02, 0x40, 0x02, 0x40, 0x0F, 0xF8, 0x1F, 0xF8, 0x1A, 0x40, 0x1A, 0x40, 0x1F, 0xF0, + 0x0F, 0xF8, 0x02, 0x58, 0x02, 0x58, 0x1F, 0xF8, 0x1F, 0xF0, 0x02, 0x40, 0x02, 0x40, 0x00, + 0x00, // $ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x0E, 0x30, 0x0E, 0x70, 0x00, 0xE0, 0x01, 0xC0, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x70, 0x0C, 0x70, 0x08, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // % + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x19, 0x80, 0x19, 0x80, 0x19, 0x80, 0x0F, 0x00, 0x0F, 0x08, + 0x0F, 0x98, 0x19, 0xF8, 0x18, 0xF0, 0x18, 0xE0, 0x19, 0xF0, 0x0F, 0x98, 0x00, 0x00, 0x00, + 0x00, // & + 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // ' + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xF0, 0x00, 0x00, 0x00, + 0x00, // ( + 0x00, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x70, + 0x00, 0x70, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, // ) + 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x11, 0x88, 0x09, 0x90, 0x07, 0xE0, 0x07, 0xE0, 0x3F, 0xFC, + 0x3F, 0xFC, 0x07, 0xE0, 0x07, 0xE0, 0x09, 0x90, 0x11, 0x88, 0x01, 0x80, 0x00, 0x00, 0x00, + 0x00, // * + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x0F, 0xF0, + 0x0F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x00, + 0x00, // , + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, + 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // - + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, // , + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, + 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, + 0x00, // / + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1C, 0x38, 0x1C, 0x78, 0x1C, 0xF8, 0x1C, 0xF8, 0x1D, 0xB8, + 0x1D, 0xB8, 0x1F, 0x38, 0x1F, 0x38, 0x1E, 0x38, 0x1C, 0x38, 0x0F, 0xF0, 0x00, 0x00, 0x00, + 0x00, // 0 + 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x03, 0x80, 0x1F, 0x80, 0x1F, 0x80, 0x03, 0x80, + 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x1F, 0xF0, 0x00, 0x00, 0x00, + 0x00, // 1 + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1C, 0x70, 0x1C, 0x38, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, + 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x38, 0x1C, 0x38, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, // 2 + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1C, 0x70, 0x1C, 0x38, 0x00, 0x38, 0x00, 0x70, 0x03, 0xC0, + 0x03, 0xC0, 0x00, 0x70, 0x00, 0x38, 0x1C, 0x38, 0x1C, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // 3 + 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xE0, 0x03, 0xE0, 0x06, 0xE0, 0x0C, 0xE0, 0x18, 0xE0, + 0x1F, 0xF8, 0x1F, 0xF8, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x03, 0xF8, 0x00, 0x00, 0x00, + 0x00, // 4 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1F, 0xE0, + 0x1F, 0xF0, 0x00, 0x78, 0x00, 0x38, 0x1C, 0x38, 0x1C, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // 5 + 0x00, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1F, 0xF0, + 0x1F, 0xF8, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x0F, 0xF0, 0x00, 0x00, 0x00, + 0x00, // 6 + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x00, 0x1C, 0x00, 0x38, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, 0x00, + 0x00, // 7 + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1F, 0x38, 0x07, 0xE0, + 0x07, 0xE0, 0x1C, 0xF8, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x0F, 0xF0, 0x00, 0x00, 0x00, + 0x00, // 8 + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1F, 0xF8, + 0x0F, 0xF8, 0x00, 0x38, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x07, 0xC0, 0x00, 0x00, 0x00, + 0x00, // 9 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // : + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // ; + 0x00, 0x00, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, + 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, + 0x00, // < + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x00, + 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // = + 0x00, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, + 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x00, + 0x00, // > + 0x00, 0x00, 0x03, 0xC0, 0x0F, 0xF0, 0x1E, 0x78, 0x18, 0x38, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, + 0x01, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x00, + 0x00, // ? + 0x00, 0x00, 0x0F, 0xF8, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0xFC, 0x1C, 0xFC, + 0x1C, 0xFC, 0x1C, 0xFC, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1F, 0xF0, 0x07, 0xF8, 0x00, + 0x00, // @ + 0x00, 0x00, 0x00, 0x00, 0x03, 0xC0, 0x07, 0xE0, 0x0E, 0x70, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, + 0x1C, 0x38, 0x1F, 0xF8, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x00, 0x00, 0x00, + 0x00, // A + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0F, 0xF0, + 0x0F, 0xF0, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x1F, 0xF0, 0x00, 0x00, 0x00, + 0x00, // B + 0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x0E, 0x38, 0x1C, 0x38, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, + 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x38, 0x0E, 0x38, 0x07, 0xF0, 0x00, 0x00, 0x00, + 0x00, // C + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x0E, 0x70, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, + 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x70, 0x1F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // D + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x0E, 0x18, 0x0E, 0x08, 0x0E, 0x00, 0x0E, 0x30, 0x0F, 0xF0, + 0x0F, 0xF0, 0x0E, 0x30, 0x0E, 0x00, 0x0E, 0x08, 0x0E, 0x18, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, // E + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x0E, 0x18, 0x0E, 0x08, 0x0E, 0x00, 0x0E, 0x30, 0x0F, 0xF0, + 0x0F, 0xF0, 0x0E, 0x30, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, + 0x00, // F + 0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x0E, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x00, 0x1C, 0x00, + 0x1C, 0x00, 0x1C, 0xF8, 0x1C, 0x38, 0x1C, 0x38, 0x0E, 0x38, 0x07, 0xF8, 0x00, 0x00, 0x00, + 0x00, // G + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1F, 0xF0, + 0x1F, 0xF0, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x00, 0x00, 0x00, + 0x00, // H + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, + 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // I + 0x00, 0x00, 0x00, 0x00, 0x01, 0xFC, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, + 0x00, 0x70, 0x38, 0x70, 0x38, 0x70, 0x38, 0x70, 0x38, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // J + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x38, 0x0E, 0x38, 0x0E, 0x70, 0x0E, 0xE0, 0x0F, 0xC0, 0x0F, 0x80, + 0x0F, 0x80, 0x0F, 0xC0, 0x0E, 0xE0, 0x0E, 0x70, 0x0E, 0x38, 0x1E, 0x38, 0x00, 0x00, 0x00, + 0x00, // K + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, + 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x08, 0x0E, 0x18, 0x0E, 0x38, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, // L + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x1E, 0x3C, 0x1F, 0x7C, 0x1F, 0xFC, 0x1F, 0xFC, 0x1D, 0xDC, + 0x1C, 0x9C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x00, 0x00, 0x00, + 0x00, // M + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1E, 0x1C, 0x1F, 0x1C, 0x1F, 0x9C, 0x1D, 0xDC, + 0x1C, 0xFC, 0x1C, 0x7C, 0x1C, 0x3C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x00, 0x00, 0x00, + 0x00, // N + 0x00, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0xF0, 0x0E, 0x38, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x0E, 0x38, 0x07, 0xF0, 0x03, 0xE0, 0x00, 0x00, 0x00, + 0x00, // O + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0F, 0xF0, + 0x0F, 0xF0, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, + 0x00, // P + 0x00, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x0F, 0x78, 0x0E, 0x38, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x7C, 0x1C, 0xFC, 0x0F, 0xF8, 0x0F, 0xF8, 0x00, 0x38, 0x00, 0xFC, 0x00, + 0x00, // Q + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF0, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0F, 0xF0, + 0x0F, 0xF0, 0x0E, 0x70, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x1E, 0x38, 0x00, 0x00, 0x00, + 0x00, // R + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x00, 0x0F, 0xE0, + 0x07, 0xF0, 0x00, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x0F, 0xF0, 0x00, 0x00, 0x00, + 0x00, // S + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x19, 0xCC, 0x11, 0xC4, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, + 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x07, 0xF0, 0x00, 0x00, 0x00, + 0x00, // T + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // U + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0E, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x00, 0x00, 0x00, + 0x00, // V + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x9C, + 0x1C, 0x9C, 0x1C, 0x9C, 0x0F, 0xF8, 0x0F, 0xF8, 0x07, 0x70, 0x07, 0x70, 0x00, 0x00, 0x00, + 0x00, // W + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0E, 0xE0, 0x07, 0xC0, 0x03, 0x80, + 0x03, 0x80, 0x07, 0xC0, 0x0E, 0xE0, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x00, 0x00, 0x00, + 0x00, // X + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0E, 0xE0, + 0x07, 0xC0, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // Y + 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1C, 0x38, 0x18, 0x38, 0x10, 0x70, 0x00, 0xE0, 0x01, 0xC0, + 0x03, 0x80, 0x07, 0x00, 0x0E, 0x08, 0x1C, 0x18, 0x1C, 0x38, 0x1F, 0xF8, 0x00, 0x00, 0x00, + 0x00, // Z + 0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, + 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0xF0, 0x00, 0x00, 0x00, + 0x00, // [ + 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x18, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, + 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x07, 0xF0, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, + 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x07, 0xF0, 0x00, 0x00, 0x00, + 0x00, // ] + 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0E, 0x70, 0x1C, 0x38, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // ^ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0x7F, + 0xFF, // _ + 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // ' + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x00, 0x70, + 0x00, 0x70, 0x0F, 0xF0, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xD8, 0x00, 0x00, 0x00, + 0x00, // a + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0F, 0xF0, 0x0E, 0x38, + 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x1B, 0xF0, 0x00, 0x00, 0x00, + 0x00, // b + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // c + 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x0F, 0xF0, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xD8, 0x00, 0x00, 0x00, + 0x00, // d + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1C, 0x70, + 0x1C, 0x70, 0x1F, 0xF0, 0x1C, 0x00, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // e + 0x00, 0x00, 0x00, 0x00, 0x03, 0xE0, 0x07, 0x70, 0x07, 0x70, 0x07, 0x00, 0x07, 0x00, 0x1F, 0xE0, + 0x1F, 0xE0, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x1F, 0xC0, 0x00, 0x00, 0x00, + 0x00, // f + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xD8, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xF0, 0x07, 0xF0, 0x00, 0x70, 0x1C, 0x70, 0x0F, + 0xE0, // g + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0xF0, 0x0F, 0x38, + 0x0F, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x1E, 0x38, 0x00, 0x00, 0x00, + 0x00, // h + 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x0F, 0xC0, 0x01, 0xC0, + 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x0F, 0xF8, 0x00, 0x00, 0x00, + 0x00, // i + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x70, + 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x00, 0x70, 0x1C, 0x70, 0x0C, 0xF0, 0x07, + 0xE0, // j + 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x38, 0x0E, 0x70, + 0x0E, 0xE0, 0x0F, 0xC0, 0x0E, 0xE0, 0x0E, 0x70, 0x0E, 0x38, 0x1E, 0x38, 0x00, 0x00, 0x00, + 0x00, // k + 0x00, 0x00, 0x00, 0x00, 0x0F, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, + 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x0F, 0xF8, 0x00, 0x00, 0x00, + 0x00, // l + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1C, 0x9C, + 0x1C, 0x9C, 0x1C, 0x9C, 0x1C, 0x9C, 0x1C, 0x9C, 0x1C, 0x9C, 0x1C, 0x9C, 0x00, 0x00, 0x00, + 0x00, // m + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x00, 0x00, 0x00, + 0x00, // n + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // o + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0xF0, 0x0E, 0x38, + 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0F, 0xF0, 0x0E, 0x00, 0x0E, 0x00, 0x1F, + 0x00, // p + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xB0, 0x38, 0xE0, + 0x38, 0xE0, 0x38, 0xE0, 0x38, 0xE0, 0x38, 0xE0, 0x1F, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x01, + 0xF0, // q + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0xF0, 0x0F, 0xF8, + 0x0F, 0x38, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, + 0x00, // r + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0x1C, 0x30, + 0x1C, 0x30, 0x0F, 0x80, 0x03, 0xE0, 0x18, 0x70, 0x18, 0x70, 0x0F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // s + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x07, 0x00, 0x1F, 0xF0, 0x07, 0x00, + 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x70, 0x07, 0x70, 0x03, 0xE0, 0x00, 0x00, 0x00, + 0x00, // t + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0F, 0xD8, 0x00, 0x00, 0x00, + 0x00, // u + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x70, 0x1C, 0x70, + 0x1C, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x0E, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x00, 0x00, 0x00, + 0x00, // v + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, + 0x1C, 0x1C, 0x1C, 0x9C, 0x1C, 0x9C, 0x0F, 0xF8, 0x07, 0x70, 0x07, 0x70, 0x00, 0x00, 0x00, + 0x00, // w + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0xE0, 0x1C, 0xE0, + 0x0F, 0xC0, 0x07, 0x80, 0x07, 0x80, 0x0F, 0xC0, 0x1C, 0xE0, 0x1C, 0xE0, 0x00, 0x00, 0x00, + 0x00, // x + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x0E, 0x38, + 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x07, 0xF0, 0x03, 0xE0, 0x00, 0xE0, 0x01, 0xC0, 0x1F, + 0x80, // y + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x18, 0xE0, + 0x11, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x20, 0x1C, 0x60, 0x1F, 0xE0, 0x00, 0x00, 0x00, + 0x00, // z + 0x00, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x07, 0x00, 0x1C, 0x00, + 0x1C, 0x00, 0x07, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x01, 0xF8, 0x00, 0x00, 0x00, + 0x00, // { + 0x00, 0x00, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, + 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x00, + 0x00, // | + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x80, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x38, + 0x00, 0x38, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0xC0, 0x01, 0xC0, 0x1F, 0x80, 0x00, 0x00, 0x00, + 0x00, // } + 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1C, 0x3B, 0x9C, 0x39, 0xDC, 0x38, 0xF8, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // ~ +]; + +/// Square 16x16 font. +pub(crate) const FONT_16X16: Font = Font { + name: "16x16", + glyph_size: LcdSize { width: WIDTH, height: HEIGHT }, + stride: 2, + data: DATA, +}; diff --git a/std/src/gfx/lcd/fonts/font_5x8.rs b/std/src/gfx/lcd/fonts/font_5x8.rs index 8e127d0a..7f32b664 100644 --- a/std/src/gfx/lcd/fonts/font_5x8.rs +++ b/std/src/gfx/lcd/fonts/font_5x8.rs @@ -921,5 +921,9 @@ const DATA: &[u8] = &[ ]; /// Small font for tiny displays. -pub(crate) const FONT_5X8: Font = - Font { name: "5x8", glyph_size: LcdSize { width: WIDTH, height: HEIGHT }, data: DATA }; +pub(crate) const FONT_5X8: Font = Font { + name: "5x8", + glyph_size: LcdSize { width: WIDTH, height: HEIGHT }, + stride: 1, + data: DATA, +}; diff --git a/std/src/gfx/lcd/fonts/mod.rs b/std/src/gfx/lcd/fonts/mod.rs index 8a4576b7..537532fb 100644 --- a/std/src/gfx/lcd/fonts/mod.rs +++ b/std/src/gfx/lcd/fonts/mod.rs @@ -21,14 +21,20 @@ use std::collections::HashMap; mod font_5x8; pub(crate) use font_5x8::FONT_5X8; +mod font_16x16; +pub(crate) use font_16x16::FONT_16X16; + /// Representation of a font. pub struct Font { /// The name of the font. - name: &'static str, + pub name: &'static str, /// The size of a single glyph, in pixels. pub glyph_size: LcdSize, + /// The number of bytes in every glyph row. + pub stride: usize, + /// The bitmap data for the font. pub data: &'static [u8], } @@ -44,7 +50,7 @@ impl Font { // characters are typically displayed. ch = '?'; } - let height = self.glyph_size.height; + let height = self.glyph_size.height * self.stride; let offset = ((ch as usize) - (' ' as usize)) * height; debug_assert!(offset < (self.data.len() + height)); &self.data[offset..offset + height] @@ -58,6 +64,7 @@ pub type Fonts = HashMap<&'static str, &'static Font>; pub fn all_fonts() -> Fonts { let mut fonts = Fonts::default(); fonts.insert(FONT_5X8.name, &FONT_5X8); + fonts.insert(FONT_16X16.name, &FONT_16X16); fonts } From 5ce94ea8e3d0bf27fd19be9702f6b9c2003497b6 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 16 Mar 2025 08:22:41 -0700 Subject: [PATCH 043/110] Git-ignore various temporary files Now that I'm using Jujutsu for development, it's too easy to end up with temporary files being automatically added to a commit. Ignore a few that are usually expected to happen. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index f8455481..aba4b12f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ Cargo.lock config.mk target +*.orig +*.rej +*~ + # Created by setup-sdl.ps1. libs dlls From 5c3099f0e212b02855d15a8747ae6e0659fc2f4c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 17 Mar 2025 05:31:41 -0700 Subject: [PATCH 044/110] Allow configuring default colors --- NEWS.md | 5 +++++ cli/src/main.rs | 5 +++-- cli/tests/cli/help.out.rpi.sdl | 5 +++-- cli/tests/cli/help.out.sdl | 3 ++- sdl/src/console.rs | 11 +++++++++-- sdl/src/host.rs | 8 ++++++-- sdl/src/lib.rs | 23 +++++++++++++++++++---- st7735s/src/lib.rs | 5 ++++- std/src/console/graphics.rs | 26 +++++++++++++++++++++----- web/src/lib.rs | 2 +- 10 files changed, 73 insertions(+), 20 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0ef20680..422c227d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -37,6 +37,11 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. paths like `LOCAL:` are flagged as invalid due to them not containing a leaf name. `KILL` was unable to remove these files after creation. +* Issue #181: Added new settings to the `sdl` and `st7735s` console drivers to + specify the default foreground and background colors. This is not yet + usable in the web interface because there is no way to configure how it + starts up yet. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/cli/src/main.rs b/cli/src/main.rs index 995855ca..2e72ea17 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -75,7 +75,8 @@ fn help(name: &str, opts: &Options) { if cfg!(feature = "sdl") { println!(" sdl[:SPEC] enables the graphical console and configures it"); println!(" with the settings in SPEC, which is of the form:"); - println!(" resolution=RESOLUTION,font_path=TTF,font_size=SIZE"); + println!(" resolution=RESOLUTION,fg_color=COLOR,bg_color=COLOR,"); + println!(" font_path=TTF,font_size=SIZE"); println!(" individual components of the SPEC can be omitted"); println!(" RESOLUTION can be one of 'fs' (for full screen),"); println!(" 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'"); @@ -83,7 +84,7 @@ fn help(name: &str, opts: &Options) { if cfg!(feature = "rpi") { println!(" st7735s[:SPEC] enables the ST7735S LCD console and configures it"); println!(" with the settings in SPEC, which is of the form:"); - println!(" font=NAME"); + println!(" fg_color=COLOR,bg_color=COLOR,font=NAME"); } println!(" text enables the text-based console"); println!(); diff --git a/cli/tests/cli/help.out.rpi.sdl b/cli/tests/cli/help.out.rpi.sdl index aa511753..91f41c83 100644 --- a/cli/tests/cli/help.out.rpi.sdl +++ b/cli/tests/cli/help.out.rpi.sdl @@ -14,13 +14,14 @@ Options: CONSOLE-SPEC can be one of the following: sdl[:SPEC] enables the graphical console and configures it with the settings in SPEC, which is of the form: - resolution=RESOLUTION,font_path=TTF,font_size=SIZE + resolution=RESOLUTION,fg_color=COLOR,bg_color=COLOR, + font_path=TTF,font_size=SIZE individual components of the SPEC can be omitted RESOLUTION can be one of 'fs' (for full screen), 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' st7735s[:SPEC] enables the ST7735S LCD console and configures it with the settings in SPEC, which is of the form: - font=NAME + fg_color=COLOR,bg_color=COLOR,font=NAME text enables the text-based console Report bugs to: https://github.com/endbasic/endbasic/issues diff --git a/cli/tests/cli/help.out.sdl b/cli/tests/cli/help.out.sdl index f1dafe54..317ae523 100644 --- a/cli/tests/cli/help.out.sdl +++ b/cli/tests/cli/help.out.sdl @@ -14,7 +14,8 @@ Options: CONSOLE-SPEC can be one of the following: sdl[:SPEC] enables the graphical console and configures it with the settings in SPEC, which is of the form: - resolution=RESOLUTION,font_path=TTF,font_size=SIZE + resolution=RESOLUTION,fg_color=COLOR,bg_color=COLOR, + font_path=TTF,font_size=SIZE individual components of the SPEC can be omitted RESOLUTION can be one of 'fs' (for full screen), 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' diff --git a/sdl/src/console.rs b/sdl/src/console.rs index 6fac305c..093dea79 100644 --- a/sdl/src/console.rs +++ b/sdl/src/console.rs @@ -43,13 +43,16 @@ pub(crate) struct SdlConsole { impl SdlConsole { /// Initializes a new SDL console. /// - /// The console is sized to `resolution` pixels. Also loads the desired font from - /// `font_path` at `font_size` and uses it to calculate the size of the console in characters. + /// The console is sized to `resolution` pixels and its default colors are set to + /// `default_fg_color` and `default_bg_color`. Also loads the desired font from `font_path` at + /// `font_size` and uses it to calculate the size of the console in characters. /// /// There can only be one active `SdlConsole` at any given time given that this initializes and /// owns the SDL context. pub(crate) fn new( resolution: Resolution, + default_fg_color: Option, + default_bg_color: Option, font_path: PathBuf, font_size: u16, signals_tx: Sender, @@ -60,6 +63,8 @@ impl SdlConsole { let handle = thread::spawn(move || { host::run( resolution, + default_fg_color, + default_bg_color, font_path, font_size, request_rx, @@ -300,6 +305,8 @@ mod testutils { NonZeroU32::new(800).unwrap(), NonZeroU32::new(600).unwrap(), )), + None, + None, src_path("sdl/src/IBMPlexMono-Regular-6.0.0.ttf"), 16, signals_chan.0, diff --git a/sdl/src/host.rs b/sdl/src/host.rs index 755ad256..0aca7b09 100644 --- a/sdl/src/host.rs +++ b/sdl/src/host.rs @@ -591,8 +591,12 @@ impl InputOps for NoopInputOps { } } +/// Runs the main graphics loop. +#[allow(clippy::too_many_arguments)] pub(crate) fn run( resolution: Resolution, + default_fg_color: Option, + default_bg_color: Option, font_path: PathBuf, font_size: u16, request_rx: Receiver, @@ -612,8 +616,8 @@ pub(crate) fn run( let mut ctx = SharedContext(Rc::from(RefCell::from(ctx))); let input = NoopInputOps {}; - let mut console = - GraphicsConsole::new(input, ctx.clone()).expect("Console initialization must succeed"); + let mut console = GraphicsConsole::new(input, ctx.clone(), default_fg_color, default_bg_color) + .expect("Console initialization must succeed"); response_tx.send(Response::Empty(Ok(()))).expect("Channel must be alive"); diff --git a/sdl/src/lib.rs b/sdl/src/lib.rs index d5719f95..a8e050aa 100644 --- a/sdl/src/lib.rs +++ b/sdl/src/lib.rs @@ -82,6 +82,9 @@ pub fn setup( Resolution::Windowed((width, height)) }); + let default_fg_color = spec.take_keyed_flag::("fg_color")?; + let default_bg_color = spec.take_keyed_flag::("bg_color")?; + let font_path = spec.take_keyed_flag::("font_path")?; let font_size = spec.take_keyed_flag("font_size")?.unwrap_or(DEFAULT_FONT_SIZE); @@ -89,13 +92,25 @@ pub fn setup( let console = match font_path { None => { let default_font = TempFont::default_font()?; - console::SdlConsole::new(resolution, default_font.path(), font_size, signals_tx)? + console::SdlConsole::new( + resolution, + default_fg_color, + default_bg_color, + default_font.path(), + font_size, + signals_tx, + )? // The console has been created at this point, so it should be safe to drop // default_font and clean up the on-disk file backing it up. } - Some(font_path) => { - console::SdlConsole::new(resolution, font_path.to_owned(), font_size, signals_tx)? - } + Some(font_path) => console::SdlConsole::new( + resolution, + default_fg_color, + default_bg_color, + font_path.to_owned(), + font_size, + signals_tx, + )?, }; Ok(Rc::from(RefCell::from(console))) } diff --git a/st7735s/src/lib.rs b/st7735s/src/lib.rs index edc0d04d..a16fc113 100644 --- a/st7735s/src/lib.rs +++ b/st7735s/src/lib.rs @@ -482,6 +482,9 @@ where B: SpiBus, K: InputOps, { + let default_fg_color = spec.take_keyed_flag::("fg_color")?; + let default_bg_color = spec.take_keyed_flag::("bg_color")?; + let font_name = spec.take_keyed_flag_str("font").unwrap_or("5x8"); let font = match fonts.get(font_name) { Some(font) => font, @@ -501,7 +504,7 @@ where let lcd = ST7735SLcd::new(pins.clone(), new_spi)?; let input = ST7735SInput::new(pins, keyboard)?; let lcd = BufferedLcd::new(lcd, font); - let inner = GraphicsConsole::new(input, lcd)?; + let inner = GraphicsConsole::new(input, lcd, default_fg_color, default_bg_color)?; Ok(ST7735SConsole { inner }) } diff --git a/std/src/console/graphics.rs b/std/src/console/graphics.rs index bb7bfebf..ae948b5e 100644 --- a/std/src/console/graphics.rs +++ b/std/src/console/graphics.rs @@ -287,6 +287,12 @@ where /// contents when the cursor moves. cursor_backup: Option, + /// Default foreground color to use. + default_fg_color: u8, + + /// Default background color to use. + default_bg_color: u8, + /// Current foreground color as exposed via `color` and `set_color`. ansi_fg_color: Option, @@ -312,9 +318,17 @@ where RO: RasterOps, { /// Initializes a new graphical console. - pub fn new(input_ops: IO, raster_ops: RO) -> io::Result { + pub fn new( + input_ops: IO, + raster_ops: RO, + default_fg_color: Option, + default_bg_color: Option, + ) -> io::Result { let info = raster_ops.get_info(); + let default_fg_color = default_fg_color.unwrap_or(DEFAULT_FG_COLOR); + let default_bg_color = default_bg_color.unwrap_or(DEFAULT_BG_COLOR); + let mut console = Self { input_ops, raster_ops, @@ -324,10 +338,12 @@ where cursor_pos: CharsXY::default(), cursor_visible: true, cursor_backup: None, + default_fg_color, + default_bg_color, ansi_bg_color: None, ansi_fg_color: None, - bg_color: ansi_color_to_rgb(DEFAULT_BG_COLOR), - fg_color: ansi_color_to_rgb(DEFAULT_FG_COLOR), + bg_color: ansi_color_to_rgb(default_bg_color), + fg_color: ansi_color_to_rgb(default_fg_color), alt_backup: None, sync_enabled: true, }; @@ -506,9 +522,9 @@ where fn set_color(&mut self, fg: Option, bg: Option) -> io::Result<()> { self.ansi_fg_color = fg; - self.fg_color = ansi_color_to_rgb(fg.unwrap_or(DEFAULT_FG_COLOR)); + self.fg_color = ansi_color_to_rgb(fg.unwrap_or(self.default_fg_color)); self.ansi_bg_color = bg; - self.bg_color = ansi_color_to_rgb(bg.unwrap_or(DEFAULT_BG_COLOR)); + self.bg_color = ansi_color_to_rgb(bg.unwrap_or(self.default_bg_color)); Ok(()) } diff --git a/web/src/lib.rs b/web/src/lib.rs index 1b9809ab..d792201c 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -226,7 +226,7 @@ impl WebTerminal { Err(e) => log_and_panic!("Console initialization failed: {}", e), }; let input_ops = WebInputOps(input); - let console = GraphicsConsole::new(input_ops, raster_ops).unwrap(); + let console = GraphicsConsole::new(input_ops, raster_ops, None, None).unwrap(); Self { yielder, console, on_screen_keyboard, service_url, signals_chan } } From c7ee64f4f725280d6688a3a9c0fb9be145f7bed4 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 17 Mar 2025 16:10:17 -0700 Subject: [PATCH 045/110] Fix color restoration in the SDL console Make sure to restore the proper colors when leaving the alternate console. The SDL driver keeps track of the current colors for efficiency and failed to restore its view of the world. This problem became apparent after making the default colors configurable: selecting custom colors, then entering the editor, then exiting it, and then running HELP would show the wrong colors. --- sdl/src/console.rs | 47 +++++++++++++++++++++++++++++++++- sdl/src/sdl-alt-colors.bmp.gz | Bin 0 -> 3423 bytes std/src/console/graphics.rs | 33 ++++++++++++++++-------- 3 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 sdl/src/sdl-alt-colors.bmp.gz diff --git a/sdl/src/console.rs b/sdl/src/console.rs index 093dea79..e9ca740e 100644 --- a/sdl/src/console.rs +++ b/sdl/src/console.rs @@ -38,6 +38,7 @@ pub(crate) struct SdlConsole { on_key_rx: Receiver, fg_color: Option, bg_color: Option, + alt_backup: Option<(Option, Option)>, } impl SdlConsole { @@ -84,6 +85,7 @@ impl SdlConsole { on_key_rx, fg_color: None, bg_color: None, + alt_backup: None, }), Response::Empty(Err(e)) => Err(e), r => panic!("Unexpected response {:?}", r), @@ -131,6 +133,15 @@ impl Console for SdlConsole { } fn enter_alt(&mut self) -> io::Result<()> { + if self.alt_backup.is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Cannot nest alternate screens", + )); + } + + self.alt_backup = Some((self.fg_color, self.bg_color)); + self.call(Request::EnterAlt) } @@ -143,7 +154,20 @@ impl Console for SdlConsole { } fn leave_alt(&mut self) -> io::Result<()> { - self.call(Request::LeaveAlt) + let alt_backup = match self.alt_backup.take() { + Some(t) => t, + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Cannot leave alternate screen; not entered", + )) + } + }; + + self.call(Request::LeaveAlt)?; + + (self.fg_color, self.bg_color) = alt_backup; + Ok(()) } fn locate(&mut self, pos: CharsXY) -> io::Result<()> { @@ -570,6 +594,27 @@ mod tests { test.verify("sdl-leave-alt"); } + #[test] + #[ignore = "Requires a graphical environment"] + fn test_sdl_console_alt_colors() { + let mut test = SdlTest::new(); + + test.console().set_color(Some(13), Some(5)).unwrap(); + + test.console().enter_alt().unwrap(); + assert_eq!((Some(13), Some(5)), test.console().color()); + test.console().print("After entering the alternate console").unwrap(); + + test.console().set_color(Some(15), Some(0)).unwrap(); + assert_eq!((Some(15), Some(0)), test.console().color()); + + test.console().leave_alt().unwrap(); + test.console().print("After leaving the alternate console").unwrap(); + assert_eq!((Some(13), Some(5)), test.console().color()); + + test.verify("sdl-alt-colors"); + } + /// Synthesizes an `Event::KeyDown` event for a single key press. fn key_down(keycode: Keycode, keymod: Mod) -> Event { Event::KeyDown { diff --git a/sdl/src/sdl-alt-colors.bmp.gz b/sdl/src/sdl-alt-colors.bmp.gz new file mode 100644 index 0000000000000000000000000000000000000000..d2f5e105ffe0365d3d3345cd0fab09bf6d0b9b88 GIT binary patch literal 3423 zcmeH|X;>228pmm@xal<2m}Z4B=A9OoTu2jBb4+a}tsc7(mJ5~Uu0*&aGInz$7rdrc zic6~tlBTFg2#zaCDw+{0W+{RZf;$Q-j5*K!a6jL#oe%G)^Stl>{LcTJKRE3lfQ_Y= zeh}Z^4bugDf7Sfc%;Y1}_wyrF zK*6UO3>Xg_4vWE()#eV}TUDS_)9Ho#{rq&Hh<5nmBJ!RottAN@FtTT9J924YxwdIx zC|1s2u8*HJMY%Ct;4ChJ#pBX%1DSX;8UEyOX=91Ndo)53l=Ccs+)ExJ}4Craj}#qEJ!exX>ge6T2R)xDq#I*zTbQav%IekL`9#7wCUF}s#jx=q6T zX#(A=GH(3BSwP<}ht?=4d4Fu}s1G>f*>pQbgC;0l7SF~e_+O1P9Mwz2cDzJ|$`u^e zJg+?}FWw-0 zFzAtyfF8zgr;e^0VkqlC>gl)vpD0?MelJUrw2S0h&19=^{`GA4rNl-AmN+z)z5JYQ zBLodwx&~y-MNkAD%p>2^%x% zmJREzta1l8q*b4g>wc-27OdQpRD^jjZI(=z6uGg&=2-K|gI<-i1cIh?JHYmH3KwzXt=n$gQ5uy7e4!l$-m38)+ItdsJ1APb{>qTvs(pJ6DXwvJMfiCx z%nhQDrD3^LDD-}7jMoh}H6N7Id8K7cYR!c&egrl`WZfk&K3nlDNoll0oVw4etV)v~ zj@He8nm}d;UXSLv(iq`#+8bZ1cI{c`2gZv_?~Ci+JZ0FO@(l7&UlgmEUAJ<0-VSGy zYl4P!pqDPr--zBWHtI{px~O-U6?N#`F7)jXd-^tO`J=FBB_Wp-ffeL zQ4OB@xuXs=t1IfU9a0$IL)d0(?7jvVxyFvAt4gP-!ASzf%6jgu=$*gpV$pbkF=$yh zK<2LCTnzT-=DIV2z?iL+&9=I%xt}K9l9{(*raW((Gs3TTbm>DF0U@ltHp*b?nGw46 z&k5jhr3KKN6G49(IbWaf&p0DeqEVG=yRVPg?66}{qI+8ssxr0_CR{+qYtJ`-Z6L0g z4Z)Qev1(l9)MQ07qZ*E_)9jBd;DuqiL`j`Rb5$+ZW=kAxpJYSOF0imrz_P;i-*dP3sYCtl%%5~ z7@}v*1Yd123)KYJK8jj8c6S-#x{+zO4BCd%*V9WOA9KRj?fp9nlXr$fzWew08izBDF6Tf literal 0 HcmV?d00001 diff --git a/std/src/console/graphics.rs b/std/src/console/graphics.rs index ae948b5e..e95fad0e 100644 --- a/std/src/console/graphics.rs +++ b/std/src/console/graphics.rs @@ -306,7 +306,8 @@ where bg_color: RGB, /// State of the console right before entering the "alternate" console. - alt_backup: Option<(RO::ID, CharsXY, RGB, RGB)>, + #[allow(clippy::type_complexity)] + alt_backup: Option<(RO::ID, CharsXY, Option, Option, RGB, RGB)>, /// Whether video syncing is enabled or not. sync_enabled: bool, @@ -537,7 +538,14 @@ where } let pixels = self.raster_ops.read_pixels(PixelsXY::new(0, 0), self.size_pixels)?; - self.alt_backup = Some((pixels, self.cursor_pos, self.fg_color, self.bg_color)); + self.alt_backup = Some(( + pixels, + self.cursor_pos, + self.ansi_fg_color, + self.ansi_bg_color, + self.fg_color, + self.bg_color, + )); self.clear(ClearType::All) } @@ -553,21 +561,24 @@ where } fn leave_alt(&mut self) -> io::Result<()> { - let (pixels, cursor_pos, fg_color, bg_color) = match self.alt_backup.take() { - Some(t) => t, - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Cannot leave alternate screen; not entered", - )) - } - }; + let (pixels, cursor_pos, ansi_fg_color, ansi_bg_color, fg_color, bg_color) = + match self.alt_backup.take() { + Some(t) => t, + None => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Cannot leave alternate screen; not entered", + )) + } + }; self.clear_cursor()?; self.raster_ops.put_pixels(PixelsXY::new(0, 0), &pixels)?; self.cursor_pos = cursor_pos; + self.ansi_fg_color = ansi_fg_color; + self.ansi_bg_color = ansi_bg_color; self.fg_color = fg_color; self.bg_color = bg_color; self.draw_cursor()?; From ffe88c5361ae038753f5a8531b113a93be7841d9 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 4 May 2025 06:46:08 -0700 Subject: [PATCH 046/110] Specify the minimum web-sys version we need The APIs that we use don't work with older web-sys 0.3 versions (I haven't checked which ones specifically) and the build breaks if the Cargo.lock is pinned to such versions. So, be specific and request a more-modern web-sys version. --- web/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/Cargo.toml b/web/Cargo.toml index 3a13e7e3..36d30fb3 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -48,7 +48,7 @@ version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.web-sys] -version = "0.3" +version = "0.3.77" features = [ "CanvasRenderingContext2d", "ContextAttributes2d", From bfd028db988463959fbd5f415f2772695f46f1da Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 28 Apr 2025 19:17:34 -0700 Subject: [PATCH 047/110] Make Drive deal with bytes A storage drive shouldn't be restricted to containing text files, and in fact, we need to support binary files if we want to store images. Switch the Drive interface to expose byte streams instead to support them. --- client/src/cmds.rs | 6 +++--- client/src/drive.rs | 33 +++++++++++++++------------------ repl/src/demos.rs | 20 ++++++++++---------- repl/src/lib.rs | 12 ++++++++++-- std/src/program.rs | 15 ++++++++++++++- std/src/storage/cmds.rs | 4 ++-- std/src/storage/fs.rs | 17 ++++++++++------- std/src/storage/mem.rs | 20 ++++++++++---------- std/src/storage/mod.rs | 34 +++++++++++++++++----------------- std/src/testutils.rs | 3 ++- web/src/store.rs | 16 ++++++++-------- 11 files changed, 101 insertions(+), 79 deletions(-) diff --git a/client/src/cmds.rs b/client/src/cmds.rs index 1ddce8ac..9abb0c97 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -901,7 +901,7 @@ mod tests { #[tokio::test] async fn test_share_print_no_acls() { let mut t = ClientTester::default(); - t.get_storage().borrow_mut().put("MEMORY:/FOO", "").await.unwrap(); + t.get_storage().borrow_mut().put("MEMORY:/FOO", b"").await.unwrap(); t.run(r#"SHARE "MEMORY:/FOO""#) .expect_prints(["", " No ACLs on MEMORY:/FOO", ""]) .expect_file("MEMORY:/FOO", "") @@ -914,7 +914,7 @@ mod tests { { let storage = t.get_storage(); let mut storage = storage.borrow_mut(); - storage.put("MEMORY:/FOO", "").await.unwrap(); + storage.put("MEMORY:/FOO", b"").await.unwrap(); storage .update_acls( "MEMORY:/FOO", @@ -933,7 +933,7 @@ mod tests { #[tokio::test] async fn test_share_make_public() { let mut t = ClientTester::default(); - t.get_storage().borrow_mut().put("MEMORY:/FOO.BAS", "").await.unwrap(); + t.get_storage().borrow_mut().put("MEMORY:/FOO.BAS", b"").await.unwrap(); t.get_service().borrow_mut().do_login().await; let mut checker = t.run(r#"SHARE "MEMORY:/FOO.BAS", "Public+r""#); let output = flatten_output(checker.take_captured_out()); diff --git a/client/src/drive.rs b/client/src/drive.rs index 627db7c3..0a366454 100644 --- a/client/src/drive.rs +++ b/client/src/drive.rs @@ -61,18 +61,12 @@ impl Drive for CloudDrive { )) } - async fn get(&self, filename: &str) -> io::Result { + async fn get(&self, filename: &str) -> io::Result> { let request = GetFileRequest::default().with_get_content(); let response = self.service.borrow_mut().get_file(&self.username, filename, &request).await?; match response.decoded_content()? { - Some(content) => match String::from_utf8(content) { - Ok(s) => Ok(s), - Err(e) => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Requested file is not valid UTF-8: {}", e), - )), - }, + Some(content) => Ok(content), None => Err(io::Error::new( io::ErrorKind::InvalidData, "Server response is missing the file content".to_string(), @@ -93,8 +87,8 @@ impl Drive for CloudDrive { } } - async fn put(&mut self, filename: &str, content: &str) -> io::Result<()> { - let request = PatchFileRequest::default().with_content(content.as_bytes()); + async fn put(&mut self, filename: &str, content: &[u8]) -> io::Result<()> { + let request = PatchFileRequest::default().with_content(content); self.service.borrow_mut().patch_file(&self.username, filename, &request).await } @@ -214,7 +208,7 @@ mod tests { }; service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); let result = drive.get("the-filename").await.unwrap(); - assert_eq!("some content", result); + assert_eq!("some content".as_bytes(), result); service.take().verify_all_used(); } @@ -235,21 +229,24 @@ mod tests { service.take().verify_all_used(); } + #[allow(invalid_from_utf8)] #[tokio::test] async fn test_clouddrive_get_invalid_utf8() { + const BAD_UTF8: &[u8] = &[0x00, 0xc3, 0x28]; + assert!(str::from_utf8(BAD_UTF8).is_err()); + let service = Rc::from(RefCell::from(MockService::default())); service.borrow_mut().do_login().await; let drive = CloudDrive::new(service.clone(), "the-user"); let request = GetFileRequest::default().with_get_content(); let response = GetFileResponse { - content: Some(BASE64_STANDARD.encode([0x00, 0xc3, 0x28])), + content: Some(BASE64_STANDARD.encode(BAD_UTF8)), ..Default::default() }; service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); - let err = drive.get("the-filename").await.unwrap_err(); - assert_eq!(io::ErrorKind::InvalidData, err.kind()); - assert!(format!("{}", err).contains("not valid UTF-8")); + let result = drive.get("the-filename").await.unwrap(); + assert_eq!(BAD_UTF8, result); service.take().verify_all_used(); } @@ -296,7 +293,7 @@ mod tests { let request = PatchFileRequest::default().with_content("some content"); service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); - drive.put("the-filename", "some content").await.unwrap(); + drive.put("the-filename", b"some content").await.unwrap(); service.take().verify_all_used(); } @@ -309,11 +306,11 @@ mod tests { let request = PatchFileRequest::default().with_content("some content"); service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); - drive.put("the-filename", "some content").await.unwrap(); + drive.put("the-filename", b"some content").await.unwrap(); let request = PatchFileRequest::default().with_content("some other content"); service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); - drive.put("the-filename", "some other content").await.unwrap(); + drive.put("the-filename", b"some other content").await.unwrap(); service.take().verify_all_used(); } diff --git a/repl/src/demos.rs b/repl/src/demos.rs index aa990895..3d40d6f3 100644 --- a/repl/src/demos.rs +++ b/repl/src/demos.rs @@ -125,18 +125,18 @@ impl Drive for DemosDrive { Ok(DriveFiles::new(entries, disk_quota, disk_free)) } - async fn get(&self, name: &str) -> io::Result { + async fn get(&self, name: &str) -> io::Result> { let uc_name = name.to_ascii_uppercase(); match self.demos.get(&uc_name.as_ref()) { Some(value) => { let (_metadata, content) = value; - Ok(content.to_string()) + Ok(content.as_bytes().to_owned()) } None => Err(io::Error::new(io::ErrorKind::NotFound, "Demo not found")), } } - async fn put(&mut self, _name: &str, _content: &str) -> io::Result<()> { + async fn put(&mut self, _name: &str, _content: &[u8]) -> io::Result<()> { Err(io::Error::new(io::ErrorKind::PermissionDenied, "The demos drive is read-only")) } } @@ -206,12 +206,12 @@ mod tests { assert_eq!(io::ErrorKind::NotFound, block_on(drive.get("unknown.bas")).unwrap_err().kind()); assert_eq!( - process_demo(include_bytes!("../examples/hello.bas")), - block_on(drive.get("hello.bas")).unwrap() + process_demo(include_bytes!("../examples/hello.bas")).as_bytes(), + block_on(drive.get("hello.bas")).unwrap().as_slice() ); assert_eq!( - process_demo(include_bytes!("../examples/hello.bas")), - block_on(drive.get("Hello.Bas")).unwrap() + process_demo(include_bytes!("../examples/hello.bas")).as_bytes(), + block_on(drive.get("Hello.Bas")).unwrap().as_slice() ); } @@ -221,16 +221,16 @@ mod tests { assert_eq!( io::ErrorKind::PermissionDenied, - block_on(drive.put("hello.bas", "")).unwrap_err().kind() + block_on(drive.put("hello.bas", b"")).unwrap_err().kind() ); assert_eq!( io::ErrorKind::PermissionDenied, - block_on(drive.put("Hello.BAS", "")).unwrap_err().kind() + block_on(drive.put("Hello.BAS", b"")).unwrap_err().kind() ); assert_eq!( io::ErrorKind::PermissionDenied, - block_on(drive.put("unknown.bas", "")).unwrap_err().kind() + block_on(drive.put("unknown.bas", b"")).unwrap_err().kind() ); } diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 7f5f2f71..bd424052 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -70,7 +70,7 @@ pub async fn try_load_autoexec( } }; - match machine.exec(&mut code.as_bytes()).await { + match machine.exec(&mut code.as_slice()).await { Ok(_) => Ok(()), Err(e) => { console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?; @@ -107,6 +107,14 @@ pub async fn run_from_cloud( console.borrow_mut().print(&format!("Loading {}...", path))?; let content = storage.borrow().get(&path).await?; + let content = match String::from_utf8(content) { + Ok(text) => text, + Err(e) => { + let mut console = console.borrow_mut(); + console.print(&format!("Invalid program to run '{}': {}", path, e))?; + return Ok(1); + } + }; program.borrow_mut().load(Some(&path), &content); console.borrow_mut().print("Starting...")?; @@ -360,7 +368,7 @@ mod tests { impl DriveFactory for MockDriveFactory { fn create(&self, target: &str) -> io::Result> { let mut drive = InMemoryDrive::default(); - block_on(drive.put(self.exp_file, Self::SCRIPT)).unwrap(); + block_on(drive.put(self.exp_file, Self::SCRIPT.as_bytes())).unwrap(); assert_eq!(self.exp_username, target); Ok(Box::from(drive)) } diff --git a/std/src/program.rs b/std/src/program.rs index ee251490..2e702c5d 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -394,6 +394,15 @@ impl Callable for LoadCommand { .make_canonical_with_extension(&pathname, DEFAULT_EXTENSION) .map_err(|e| scope.io_error(e))?; let content = storage.get(&full_name).await.map_err(|e| scope.io_error(e))?; + let content = match String::from_utf8(content) { + Ok(text) => text, + Err(e) => { + return Err(scope.io_error(io::Error::new( + io::ErrorKind::InvalidData, + format!("Invalid file content: {}", e), + ))); + } + }; (full_name, content) }; self.program.borrow_mut().load(Some(&full_name), &content); @@ -593,7 +602,11 @@ impl Callable for SaveCommand { .make_canonical_with_extension(&name, DEFAULT_EXTENSION) .map_err(|e| scope.io_error(e))?; let content = self.program.borrow().text(); - self.storage.borrow_mut().put(&full_name, &content).await.map_err(|e| scope.io_error(e))?; + self.storage + .borrow_mut() + .put(&full_name, content.as_bytes()) + .await + .map_err(|e| scope.io_error(e))?; self.program.borrow_mut().set_name(&full_name); self.console diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index ce55ee0c..32e51284 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -503,7 +503,7 @@ mod tests { #[test] fn test_dir_other_by_argument() { let mut other = InMemoryDrive::default(); - block_on(other.put("foo.bas", "hello")).unwrap(); + block_on(other.put("foo.bas", b"hello")).unwrap(); let mut t = Tester::default().write_file("empty.bas", ""); t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap(); @@ -544,7 +544,7 @@ mod tests { #[test] fn test_dir_other_by_cwd() { let mut other = InMemoryDrive::default(); - block_on(other.put("foo.bas", "hello")).unwrap(); + block_on(other.put("foo.bas", b"hello")).unwrap(); let mut t = Tester::default().write_file("empty.bas", ""); t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap(); diff --git a/std/src/storage/fs.rs b/std/src/storage/fs.rs index 6ba79665..f7259389 100644 --- a/std/src/storage/fs.rs +++ b/std/src/storage/fs.rs @@ -98,18 +98,18 @@ impl Drive for DirectoryDrive { Ok(DriveFiles::new(entries, None, None)) } - async fn get(&self, name: &str) -> io::Result { + async fn get(&self, name: &str) -> io::Result> { let path = self.dir.join(name); let input = File::open(path)?; - let mut content = String::new(); - io::BufReader::new(input).read_to_string(&mut content)?; + let mut content = vec![]; + io::BufReader::new(input).read_to_end(&mut content)?; Ok(content) } - async fn put(&mut self, name: &str, content: &str) -> io::Result<()> { + async fn put(&mut self, name: &str, content: &[u8]) -> io::Result<()> { let path = self.dir.join(name); let mut output = OpenOptions::new().create(true).write(true).truncate(true).open(path)?; - output.write_all(content.as_bytes())?; + output.write_all(content)?; output.sync_all() } @@ -266,7 +266,10 @@ mod tests { write_file(&dir.path().join("some file.bas"), &["one line", "two lines"]); let drive = DirectoryDrive::new(dir.path()).unwrap(); - assert_eq!("one line\ntwo lines\n", block_on(drive.get("some file.bas")).unwrap()); + assert_eq!( + b"one line\ntwo lines\n", + block_on(drive.get("some file.bas")).unwrap().as_slice() + ); } #[test] @@ -274,7 +277,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let mut drive = DirectoryDrive::new(dir.path()).unwrap(); - block_on(drive.put("some file.bas", "a b c\nd e\n")).unwrap(); + block_on(drive.put("some file.bas", b"a b c\nd e\n")).unwrap(); check_file(&dir.path().join("some file.bas"), &["a b c", "d e"]); } diff --git a/std/src/storage/mem.rs b/std/src/storage/mem.rs index a747769a..b80727cd 100644 --- a/std/src/storage/mem.rs +++ b/std/src/storage/mem.rs @@ -24,7 +24,7 @@ use std::str; /// A drive that records all data in memory only. #[derive(Default)] pub struct InMemoryDrive { - programs: HashMap)>, + programs: HashMap, HashSet)>, // TODO(jmmv): These fields are currently exposed only to allow testing for the consumers of // these details and are not enforced in the drive. It might be nice to actually implement @@ -52,7 +52,7 @@ impl Drive for InMemoryDrive { Ok(DriveFiles::new(entries, self.fake_disk_quota, self.fake_disk_free)) } - async fn get(&self, name: &str) -> io::Result { + async fn get(&self, name: &str) -> io::Result> { match self.programs.get(name) { Some((content, _readers)) => Ok(content.to_owned()), None => Err(io::Error::new(io::ErrorKind::NotFound, "Entry not found")), @@ -72,7 +72,7 @@ impl Drive for InMemoryDrive { } } - async fn put(&mut self, name: &str, content: &str) -> io::Result<()> { + async fn put(&mut self, name: &str, content: &[u8]) -> io::Result<()> { if let Some((prev_content, _readers)) = self.programs.get_mut(name) { content.clone_into(prev_content); return Ok(()); @@ -130,30 +130,30 @@ mod tests { #[tokio::test] async fn test_inmemorydrive_put_respects_acls() { let mut drive = InMemoryDrive::default(); - drive.put("untouched", "some content").await.unwrap(); + drive.put("untouched", b"some content").await.unwrap(); - drive.put("file", "before").await.unwrap(); + drive.put("file", b"before").await.unwrap(); drive.update_acls("file", &readers(&["r1"]), &FileAcls::default()).await.unwrap(); - assert_eq!("before", drive.get("file").await.unwrap()); + assert_eq!(b"before", drive.get("file").await.unwrap().as_slice()); assert_eq!(readers(&["r1"]), drive.get_acls("file").await.unwrap()); - drive.put("file", "after").await.unwrap(); + drive.put("file", b"after").await.unwrap(); - assert_eq!("after", drive.get("file").await.unwrap()); + assert_eq!(b"after", drive.get("file").await.unwrap().as_slice()); assert_eq!(readers(&["r1"]), drive.get_acls("file").await.unwrap()); } #[tokio::test] async fn test_inmemorydrive_get_update_acls() { let mut drive = InMemoryDrive::default(); - drive.put("untouched", "some content").await.unwrap(); + drive.put("untouched", b"some content").await.unwrap(); let err = drive.update_acls("file", &readers(&["r0"]), &FileAcls::default()).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind()); - drive.put("file", "some content").await.unwrap(); + drive.put("file", b"some content").await.unwrap(); assert_eq!(FileAcls::default(), drive.get_acls("file").await.unwrap()); // Add some new readers and try to remove a non-existing one. diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index aec89863..8999af16 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -150,7 +150,7 @@ pub trait Drive { async fn enumerate(&self) -> io::Result; /// Loads the contents of the program given by `name`. - async fn get(&self, name: &str) -> io::Result; + async fn get(&self, name: &str) -> io::Result>; /// Gets the ACLs of the file `_name`. async fn get_acls(&self, _name: &str) -> io::Result { @@ -158,7 +158,7 @@ pub trait Drive { } /// Saves the in-memory program given by `content` into `name`. - async fn put(&mut self, name: &str, content: &str) -> io::Result<()>; + async fn put(&mut self, name: &str, content: &[u8]) -> io::Result<()>; /// Updates the ACLs of the file `_name` by extending them with the contents of `_add` and /// removing the existing entries listed in `_remove`. @@ -589,7 +589,7 @@ impl Storage { } /// Loads the contents of the program given by `raw_location`. - pub async fn get(&self, raw_location: &str) -> io::Result { + pub async fn get(&self, raw_location: &str) -> io::Result> { let location = Location::new(raw_location)?; match location.leaf_name() { Some(name) => self.get_drive(&location)?.get(name).await, @@ -613,7 +613,7 @@ impl Storage { } /// Saves the in-memory program given by `content` into `raw_location`. - pub async fn put(&mut self, raw_location: &str, content: &str) -> io::Result<()> { + pub async fn put(&mut self, raw_location: &str, content: &[u8]) -> io::Result<()> { let location = Location::new(raw_location)?; match location.leaf_name() { Some(name) => self.get_drive_mut(&location)?.put(name, content).await, @@ -913,8 +913,8 @@ mod tests { storage.mount("c", &format!("file://{}", dir1.display())).unwrap(); storage.mount("d", &format!("file://{}", dir2.display())).unwrap(); - block_on(storage.put("c:file1.txt", "hi")).unwrap(); - block_on(storage.put("d:file2.txt", "bye")).unwrap(); + block_on(storage.put("c:file1.txt", b"hi")).unwrap(); + block_on(storage.put("d:file2.txt", b"bye")).unwrap(); assert!(dir1.join("file1.txt").exists()); assert!(!dir1.join("file2.txt").exists()); @@ -1012,8 +1012,8 @@ mod tests { let mut storage = Storage::default(); storage.mount("other", "memory://").unwrap(); - block_on(storage.put("other:/f1", "some text")).unwrap(); - block_on(storage.put("other:f2", "other text")).unwrap(); + block_on(storage.put("other:/f1", b"some text")).unwrap(); + block_on(storage.put("other:f2", b"other text")).unwrap(); { // Ensure that the put operations were routed to the correct objects. let memory_drive = storage.drives.get(&DriveKey::new("memory").unwrap()).unwrap(); @@ -1027,8 +1027,8 @@ mod tests { assert_eq!(2, block_on(storage.enumerate("other:/")).unwrap().dirents().len()); assert_eq!(2, block_on(storage.enumerate("other:/")).unwrap().dirents().len()); - assert_eq!("some text", block_on(storage.get("OTHER:f1")).unwrap()); - assert_eq!("other text", block_on(storage.get("OTHER:/f2")).unwrap()); + assert_eq!(b"some text", block_on(storage.get("OTHER:f1")).unwrap().as_slice()); + assert_eq!(b"other text", block_on(storage.get("OTHER:/f2")).unwrap().as_slice()); block_on(storage.delete("other:/f2")).unwrap(); assert_eq!(0, block_on(storage.enumerate("memory:")).unwrap().dirents().len()); @@ -1043,8 +1043,8 @@ mod tests { let mut storage = Storage::default(); storage.mount("other", "memory://").unwrap(); - block_on(storage.put("/f1", "some text")).unwrap(); - block_on(storage.put("f2", "other text")).unwrap(); + block_on(storage.put("/f1", b"some text")).unwrap(); + block_on(storage.put("f2", b"other text")).unwrap(); { // Ensure that the put operations were routed to the correct objects. let memory_drive = storage.drives.get(&DriveKey::new("memory").unwrap()).unwrap(); @@ -1058,8 +1058,8 @@ mod tests { assert_eq!(0, block_on(storage.enumerate("other:")).unwrap().dirents().len()); assert_eq!(0, block_on(storage.enumerate("other:/")).unwrap().dirents().len()); - assert_eq!("some text", block_on(storage.get("f1")).unwrap()); - assert_eq!("other text", block_on(storage.get("/f2")).unwrap()); + assert_eq!(b"some text", block_on(storage.get("f1")).unwrap().as_slice()); + assert_eq!(b"other text", block_on(storage.get("/f2")).unwrap().as_slice()); block_on(storage.delete("/f2")).unwrap(); assert_eq!(1, block_on(storage.enumerate("")).unwrap().dirents().len()); @@ -1125,15 +1125,15 @@ mod tests { let mut storage = Storage::default(); assert_eq!( "Invalid drive name ''", - format!("{}", block_on(storage.put(":foo", "")).unwrap_err()) + format!("{}", block_on(storage.put(":foo", b"")).unwrap_err()) ); assert_eq!( "Invalid path 'a:b\\c'", - format!("{}", block_on(storage.put("a:b\\c", "")).unwrap_err()) + format!("{}", block_on(storage.put("a:b\\c", b"")).unwrap_err()) ); assert_eq!( "Missing file name in path 'a:'", - format!("{}", block_on(storage.put("a:", "")).unwrap_err()) + format!("{}", block_on(storage.put("a:", b"")).unwrap_err()) ); } diff --git a/std/src/testutils.rs b/std/src/testutils.rs index 23a96f40..28fc10cf 100644 --- a/std/src/testutils.rs +++ b/std/src/testutils.rs @@ -501,7 +501,7 @@ impl Tester { /// Creates or overwrites a file in the storage medium. pub fn write_file(self, name: &str, content: &str) -> Self { - block_on(self.storage.borrow_mut().put(name, content)).unwrap(); + block_on(self.storage.borrow_mut().put(name, content.as_bytes())).unwrap(); self } @@ -748,6 +748,7 @@ impl<'a> Checker<'a> { for name in block_on(storage.enumerate(&root)).unwrap().dirents().keys() { let path = format!("{}{}", root, name); let content = block_on(storage.get(&path)).unwrap(); + let content = String::from_utf8(content).unwrap(); files.insert(path, content); } } diff --git a/web/src/store.rs b/web/src/store.rs index f660a2eb..6c98b7d0 100644 --- a/web/src/store.rs +++ b/web/src/store.rs @@ -96,7 +96,7 @@ struct Entry { version: u16, /// The textual content of the program. - content: String, + content: Vec, /// The last modification time of the program, in UTC. mtime: OffsetDateTime, @@ -107,7 +107,7 @@ impl Entry { const VERSION: u16 = 1; /// Constructs a new entry with the given `content` and with a last modification of now. - fn new>(content: S, mtime: OffsetDateTime) -> Self { + fn new>>(content: S, mtime: OffsetDateTime) -> Self { Self { version: Entry::VERSION, content: content.into(), mtime } } @@ -281,12 +281,12 @@ impl Drive for WebDrive { Ok(DriveFiles::new(entries, None, None)) } - async fn get(&self, name: &str) -> io::Result { + async fn get(&self, name: &str) -> io::Result> { let entry = self.get_entry(&Key::for_name(name))?; Ok(entry.content) } - async fn put(&mut self, name: &str, content: &str) -> io::Result<()> { + async fn put(&mut self, name: &str, content: &[u8]) -> io::Result<()> { let key = Key::for_name(name); // There is no information we care about the old entry so we can replace it all in one go @@ -415,12 +415,12 @@ mod tests { async fn test_webdrive_enumerate() { let entry1 = Entry { version: Entry::VERSION, - content: "first".to_owned(), + content: b"first".to_vec(), mtime: OffsetDateTime::from_unix_timestamp(1234).unwrap(), }; let entry2 = Entry { version: Entry::VERSION, - content: "second".to_owned(), + content: b"second".to_vec(), mtime: OffsetDateTime::from_unix_timestamp(987_654_321).unwrap(), }; @@ -447,7 +447,7 @@ mod tests { async fn test_webdrive_get() { let entry = Entry { version: Entry::VERSION, - content: "second".to_owned(), + content: b"second".to_vec(), mtime: OffsetDateTime::from_unix_timestamp(1234).unwrap(), }; @@ -468,7 +468,7 @@ mod tests { async fn test_webdrive_put() { let entry = Entry { version: Entry::VERSION, - content: "this is some content".to_owned(), + content: b"this is some content".to_vec(), mtime: OffsetDateTime::from_unix_timestamp(1_234_567).unwrap(), }; From 13a0d8b8c0b113d6ab96e7f5b38352465de64443 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 2 May 2025 07:10:30 -0700 Subject: [PATCH 048/110] Use new header-based "get file" service API This API was rewritten to support the web-based dynamic gallery in the EndBASIC website, and we should use it here too so that we can deprecate the old code paths. One benefit of this API is that it avoids the use of base64 for the file content, but I can't fully eliminate base64 yet because "patch file" needs it too at this point. --- client/src/cloud.rs | 78 ++++++++++++++++++++++++++--------------- client/src/drive.rs | 70 +++++++++++------------------------- client/src/lib.rs | 65 +++++----------------------------- client/src/testutils.rs | 49 +++++++++++++++++--------- 4 files changed, 109 insertions(+), 153 deletions(-) diff --git a/client/src/cloud.rs b/client/src/cloud.rs index 51335035..ba895c72 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -19,6 +19,7 @@ use crate::*; use async_trait::async_trait; 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; @@ -248,26 +249,54 @@ impl Service for CloudService { } } - async fn get_file( - &mut self, - username: &str, - filename: &str, - request: &GetFileRequest, - ) -> io::Result { + async fn get_file(&mut self, username: &str, filename: &str) -> io::Result> { let mut builder = self .client .get(self.make_url(&format!("api/users/{}/files/{}", username, filename))) - .headers(self.default_headers()) - .query(&request); + .headers(self.default_headers()); if let Some(auth_data) = self.auth_data.borrow().as_ref() { builder = builder.bearer_auth(auth_data.access_token.as_str()); } let response = builder.send().await.map_err(reqwest_error_to_io_error)?; match response.status() { StatusCode::OK => { + Ok(response.bytes().await.map_err(reqwest_error_to_io_error)?.to_vec()) + } + _ => Err(http_response_to_io_error(response).await), + } + } + + async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result { + let mut headers = self.default_headers(); + headers.insert("X-EndBASIC-GetContent", "false".parse().unwrap()); + headers.insert("X-EndBASIC-GetReaders", "true".parse().unwrap()); + let mut builder = self + .client + .get(self.make_url(&format!("api/users/{}/files/{}", username, filename))) + .headers(headers); + if let Some(auth_data) = self.auth_data.borrow().as_ref() { + builder = builder.bearer_auth(auth_data.access_token.as_str()); + } + let response = builder.send().await.map_err(reqwest_error_to_io_error)?; + match response.status() { + StatusCode::OK => { + let mut readers = vec![]; + for h in response.headers().get_all("X-EndBASIC-Reader") { + match h.to_str() { + Ok(value) => readers.push(value.to_owned()), + Err(e) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Server returned invalid reader ACL: {}", e), + )) + } + } + } + let bytes = response.bytes().await.map_err(reqwest_error_to_io_error)?; - let response: GetFileResponse = serde_json::from_reader(bytes.reader())?; - Ok(response) + debug_assert!(bytes.is_empty(), "Did not expect server to return content"); + + Ok(FileAcls::default().with_readers(readers)) } _ => Err(http_response_to_io_error(response).await), } @@ -480,8 +509,7 @@ mod tests { assert!(disk_free.files() >= needed_files, "Not enough space for test run"); for (filename, _content) in &filenames_and_contents { - let request = GetFileRequest::default().with_get_content(); - let err = service.get_file(&username, filename, &request).await.unwrap_err(); + let err = service.get_file(&username, filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); } @@ -505,9 +533,7 @@ mod tests { let request = PatchFileRequest::default().with_content(content); service.patch_file(&username, filename, &request).await.unwrap(); - let request = GetFileRequest::default().with_get_content(); - let response = service.get_file(&username, filename, &request).await.unwrap(); - assert_eq!(content, response.decoded_content().unwrap().unwrap()); + assert_eq!(content, service.get_file(&username, filename).await.unwrap()); } #[test] @@ -553,8 +579,7 @@ mod tests { let mut service = context.service(); let (filename, _content) = context.random_file(); - let request = GetFileRequest::default().with_get_content(); - let err = service.get_file(&username, &filename, &request).await.unwrap_err(); + let err = service.get_file(&username, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); } run(&mut TestContext::new_from_env()); @@ -599,8 +624,7 @@ mod tests { // Read username1's file as username2 before it is shared. context.do_login(2).await; - let request = GetFileRequest::default().with_get_content(); - let err = service.get_file(&username1, &filename, &request).await.unwrap_err(); + let err = service.get_file(&username1, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); // Share username1's file with username2. @@ -610,9 +634,8 @@ mod tests { // Read username1's file as username2 again, now that it is shared. context.do_login(2).await; - let request = GetFileRequest::default().with_get_content(); - let response = service.get_file(&username1, &filename, &request).await.unwrap(); - assert_eq!(content.as_bytes(), response.decoded_content().unwrap().unwrap()); + let response = service.get_file(&username1, &filename).await.unwrap(); + assert_eq!(content.as_bytes(), response); } run(&mut TestContext::new_from_env()); } @@ -635,8 +658,7 @@ mod tests { // Read username1's file as a guest before it is shared. context.do_logout().await; - let request = GetFileRequest::default().with_get_content(); - let err = service.get_file(&username1, &filename, &request).await.unwrap_err(); + let err = service.get_file(&username1, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); // Share username1's file with the public. @@ -646,9 +668,8 @@ mod tests { // Read username1's file as a guest again, now that it is shared. context.do_logout().await; - let request = GetFileRequest::default().with_get_content(); - let response = service.get_file(&username1, &filename, &request).await.unwrap(); - assert_eq!(content.as_bytes(), response.decoded_content().unwrap().unwrap()); + let response = service.get_file(&username1, &filename).await.unwrap(); + assert_eq!(content.as_bytes(), response); } run(&mut TestContext::new_from_env()); } @@ -667,8 +688,7 @@ mod tests { service.delete_file(&username, &filename).await.unwrap(); - let request = GetFileRequest::default().with_get_content(); - let err = service.get_file(&username, &filename, &request).await.unwrap_err(); + let err = service.get_file(&username, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); assert!(format!("{}", err).contains("(server code: 404")); } diff --git a/client/src/drive.rs b/client/src/drive.rs index 0a366454..e4230607 100644 --- a/client/src/drive.rs +++ b/client/src/drive.rs @@ -62,29 +62,11 @@ impl Drive for CloudDrive { } async fn get(&self, filename: &str) -> io::Result> { - let request = GetFileRequest::default().with_get_content(); - let response = - self.service.borrow_mut().get_file(&self.username, filename, &request).await?; - match response.decoded_content()? { - Some(content) => Ok(content), - None => Err(io::Error::new( - io::ErrorKind::InvalidData, - "Server response is missing the file content".to_string(), - )), - } + self.service.borrow_mut().get_file(&self.username, filename).await } async fn get_acls(&self, filename: &str) -> io::Result { - let request = GetFileRequest::default().with_get_readers(); - let response = - self.service.borrow_mut().get_file(&self.username, filename, &request).await?; - match response.readers { - Some(readers) => Ok(FileAcls::default().with_readers(readers)), - None => Err(io::Error::new( - io::ErrorKind::InvalidData, - "Server response is missing the readers list".to_string(), - )), - } + self.service.borrow_mut().get_file_acls(&self.username, filename).await } async fn put(&mut self, filename: &str, content: &[u8]) -> io::Result<()> { @@ -201,12 +183,11 @@ mod tests { service.borrow_mut().do_login().await; let drive = CloudDrive::new(service.clone(), "the-user"); - let request = GetFileRequest::default().with_get_content(); - let response = GetFileResponse { - content: Some(BASE64_STANDARD.encode("some content")), - ..Default::default() - }; - service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); + service.borrow_mut().add_mock_get_file( + "the-user", + "the-filename", + Ok(b"some content".to_owned()), + ); let result = drive.get("the-filename").await.unwrap(); assert_eq!("some content".as_bytes(), result); @@ -219,12 +200,9 @@ mod tests { service.borrow_mut().do_login().await; let drive = CloudDrive::new(service.clone(), "the-user"); - let request = GetFileRequest::default().with_get_content(); - let response = GetFileResponse::default(); - service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); - let err = drive.get("the-filename").await.unwrap_err(); - assert_eq!(io::ErrorKind::InvalidData, err.kind()); - assert!(format!("{}", err).contains("missing the file content")); + service.borrow_mut().add_mock_get_file("the-user", "the-filename", Ok("")); + let result = drive.get("the-filename").await.unwrap(); + assert_eq!("".as_bytes(), result); service.take().verify_all_used(); } @@ -239,12 +217,7 @@ mod tests { service.borrow_mut().do_login().await; let drive = CloudDrive::new(service.clone(), "the-user"); - let request = GetFileRequest::default().with_get_content(); - let response = GetFileResponse { - content: Some(BASE64_STANDARD.encode(BAD_UTF8)), - ..Default::default() - }; - service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); + service.borrow_mut().add_mock_get_file("the-user", "the-filename", Ok(BAD_UTF8)); let result = drive.get("the-filename").await.unwrap(); assert_eq!(BAD_UTF8, result); @@ -257,12 +230,8 @@ mod tests { service.borrow_mut().do_login().await; let drive = CloudDrive::new(service.clone(), "the-user"); - let request = GetFileRequest::default().with_get_readers(); - let response = GetFileResponse { - readers: Some(vec!["r1".to_owned(), "r2".to_owned()]), - ..Default::default() - }; - service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); + let response = FileAcls { readers: vec!["r1".to_owned(), "r2".to_owned()] }; + service.borrow_mut().add_mock_get_file_acls("the-user", "the-filename", Ok(response)); let result = drive.get_acls("the-filename").await.unwrap(); assert_eq!(FileAcls::default().with_readers(["r1".to_owned(), "r2".to_owned()]), result); @@ -275,12 +244,13 @@ mod tests { service.borrow_mut().do_login().await; let drive = CloudDrive::new(service.clone(), "the-user"); - let request = GetFileRequest::default().with_get_readers(); - let response = GetFileResponse::default(); - service.borrow_mut().add_mock_get_file("the-user", "the-filename", request, Ok(response)); - let err = drive.get_acls("the-filename").await.unwrap_err(); - assert_eq!(io::ErrorKind::InvalidData, err.kind()); - assert!(format!("{}", err).contains("missing the readers list")); + service.borrow_mut().add_mock_get_file_acls( + "the-user", + "the-filename", + Ok(FileAcls::default()), + ); + let result = drive.get_acls("the-filename").await.unwrap(); + assert_eq!(FileAcls::default(), result); service.take().verify_all_used(); } diff --git a/client/src/lib.rs b/client/src/lib.rs index 7107491e..89200ae3 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -24,7 +24,7 @@ use async_trait::async_trait; use base64::prelude::*; -use endbasic_std::storage::DiskSpace; +use endbasic_std::storage::{DiskSpace, FileAcls}; use serde::{Deserialize, Serialize}; use std::io; @@ -112,54 +112,6 @@ pub struct GetFilesResponse { disk_free: Option, } -/// Representation of a file query. -#[derive(Debug, Default, Eq, PartialEq, Serialize)] -#[cfg_attr(test, derive(Deserialize))] -pub struct GetFileRequest { - get_content: bool, - get_readers: bool, -} - -impl GetFileRequest { - /// Requests the file's content from the server. - fn with_get_content(mut self) -> Self { - self.get_content = true; - self - } - - /// Requests the file's readers from the server. - fn with_get_readers(mut self) -> Self { - self.get_readers = true; - self - } -} - -/// Representation of the response to a file query. -#[derive(Default, Deserialize)] -#[cfg_attr(test, derive(Debug, PartialEq, Serialize))] -pub struct GetFileResponse { - /// Base64-encoded file content. - content: Option, - - readers: Option>, -} - -impl GetFileResponse { - /// Processes the content of the response, ensuring it is valid base64. - fn decoded_content(&self) -> io::Result>> { - match self.content.as_ref() { - Some(content) => match BASE64_STANDARD.decode(content) { - Ok(content) => Ok(Some(content)), - Err(e) => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("File content is not properly base64-encoded: {}", e), - )), - }, - None => Ok(None), - } - } -} - /// Representation of an atomic file update. #[derive(Debug, Default, Eq, PartialEq, Serialize)] #[cfg_attr(test, derive(Deserialize))] @@ -228,14 +180,13 @@ pub trait Service { /// previously-acquired `access_token`. async fn get_files(&mut self, username: &str) -> io::Result; - /// Sends a request to the server to obtain the metadata and/or the contents of `filename` owned - /// by `username` as specified in `request` with a previously-acquired `access_token`. - async fn get_file( - &mut self, - username: &str, - filename: &str, - request: &GetFileRequest, - ) -> io::Result; + /// Sends a request to the server to obtain the contents of `filename` owned by `username` with a + /// previously-acquired `access_token`. + async fn get_file(&mut self, username: &str, filename: &str) -> io::Result>; + + /// Sends a request to the server to obtain the ACLs of `filename` owned by `username` with a + /// previously-acquired `access_token`. + async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result; /// Sends a request to the server to update the metadata and/or the contents of `filename` owned /// by `username` as specified in `request` with a previously-acquired `access_token`. diff --git a/client/src/testutils.rs b/client/src/testutils.rs index 9b97ada7..4e6debf9 100644 --- a/client/src/testutils.rs +++ b/client/src/testutils.rs @@ -16,11 +16,10 @@ //! Test utilities for the cloud service. use crate::{ - add_all, AccessToken, GetFileRequest, GetFileResponse, GetFilesResponse, LoginResponse, - PatchFileRequest, Service, SignupRequest, + add_all, AccessToken, GetFilesResponse, LoginResponse, PatchFileRequest, Service, SignupRequest, }; use async_trait::async_trait; -use endbasic_std::storage::Storage; +use endbasic_std::storage::{FileAcls, Storage}; use endbasic_std::testutils::*; use std::cell::RefCell; use std::collections::VecDeque; @@ -36,7 +35,8 @@ pub struct MockService { mock_signup: VecDeque<(SignupRequest, io::Result<()>)>, mock_login: VecDeque<((String, String), io::Result)>, mock_get_files: VecDeque<(String, io::Result)>, - mock_get_file: VecDeque<((String, String, GetFileRequest), io::Result)>, + mock_get_file: VecDeque<((String, String), io::Result>)>, + mock_get_file_acls: VecDeque<((String, String), io::Result)>, mock_patch_file: VecDeque<((String, String, PatchFileRequest), io::Result<()>)>, mock_delete_file: VecDeque<((String, String), io::Result<()>)>, } @@ -86,17 +86,29 @@ impl MockService { } /// Records the behavior of an upcoming "get file" operation for the `username`/`filename` - /// pair with a request that looks like `exp_request` and that returns `result`. + /// pair that returns `result`. #[cfg(test)] - pub(crate) fn add_mock_get_file( + pub(crate) fn add_mock_get_file>>( &mut self, username: &str, filename: &str, - exp_request: GetFileRequest, - result: io::Result, + result: io::Result, ) { - let exp_request = (username.to_owned(), filename.to_owned(), exp_request); - self.mock_get_file.push_back((exp_request, result)); + let exp_request = (username.to_owned(), filename.to_owned()); + self.mock_get_file.push_back((exp_request, result.map(|b| b.into()))); + } + + /// Records the behavior of an upcoming "get file ACLs" operation for the `username`/`filename` + /// pair that returns `result`. + #[cfg(test)] + pub(crate) fn add_mock_get_file_acls( + &mut self, + username: &str, + filename: &str, + result: io::Result, + ) { + let exp_request = (username.to_owned(), filename.to_owned()); + self.mock_get_file_acls.push_back((exp_request, result)); } /// Records the behavior of an upcoming "patch file" operation for the `username`/`filename` @@ -181,18 +193,21 @@ impl Service for MockService { mock.1 } - async fn get_file( - &mut self, - username: &str, - filename: &str, - request: &GetFileRequest, - ) -> io::Result { + async fn get_file(&mut self, username: &str, filename: &str) -> io::Result> { 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 .2, request); + mock.1 + } + + async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result { + 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); mock.1 } From 6afac813488b0785ab26c2cd597a5e8887b4a2c1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 2 May 2025 10:51:10 -0700 Subject: [PATCH 049/110] Move KILL to storage This puts the KILL command where it belongs and, as a result, addresses a long-standing TODO: KILL will no longer automatically adds the BAS extension to extension-less filenames. This allows deleting files created without an extension, which may be necessary today and will definitely be necessary when I add a COPY command next. --- NEWS.md | 4 ++ cli/tests/repl/help.out | 7 ++- std/README.md | 4 +- std/src/program.rs | 92 --------------------------------------- std/src/storage/cmds.rs | 96 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 98 deletions(-) diff --git a/NEWS.md b/NEWS.md index 422c227d..ba309e7a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -42,6 +42,10 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. usable in the web interface because there is no way to configure how it starts up yet. +* Changed the `KILL` command so that it no longer automatically appends a + `.BAS` extension to filenames given to it. This allow deleting files that + don't have an extension. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/cli/tests/repl/help.out b/cli/tests/repl/help.out index 5d8e60ca..2d1dec5c 100644 --- a/cli/tests/repl/help.out +++ b/cli/tests/repl/help.out @@ -146,6 +146,7 @@ Output from HELP "FILE SYSTEM": >> CD  Changes the current path. >> DIR  Displays the list of files on the current or given path. + >> KILL  Deletes the given file. >> MOUNT  Lists the mounted drives or mounts a new drive. >> PWD  Prints the current working location. >> UNMOUNT Unmounts the given drive. @@ -267,7 +268,6 @@ Output from HELP "STORED": >> DISASM Disassembles the stored program. >> EDIT  Interactively edits the stored program. - >> KILL  Deletes the given program. >> LIST  Prints the currently-loaded program. >> LOAD  Loads the given program. >> NEW  Restores initial machine state and creates a new program. @@ -851,10 +851,9 @@ Output from HELP "KILL":  KILL filename$  - Deletes the given program. + Deletes the given file. - The filename must be a string and must be a valid EndBASIC path. The - .BAS extension is optional but, if present, it must be .BAS. + The filename must be a string and must be a valid EndBASIC path. See the "File system" help topic for information on the path syntax. diff --git a/std/README.md b/std/README.md index 8aa63285..cd2c211c 100644 --- a/std/README.md +++ b/std/README.md @@ -40,11 +40,11 @@ not intend to be fully compatible with them. The library currently contains: * Graphics: `GFX_CIRCLE`, `GFX_CIRCLEF`, `GFX_HEIGHT`, `GFX_LINE`, `GFX_PIXEL`, `GFX_RECT`, `GFX_RECTF`, `GFX_SYNC`, `GFX_WIDTH`. * Hardware interaction: `GPIO_CLEAR`, `GPIO_READ`, `GPIO_SETUP`, `GPIO_WRITE`. -* File system interaction: `CD`, `DIR`, `MOUNT`, `PWD`, `UNMOUNT`. +* File system interaction: `CD`, `DIR`, `KILL`, `MOUNT`, `PWD`, `UNMOUNT`. * Interpreter interaction: `CLEAR`, `ERRMSG`, `HELP`. * Numerics: `ATN`, `CINT`, `COS`, `DEG`, `INT`, `MAX`, `MIN`, `PI`, `RAD`, `RANDOMIZE`, `RND`, `SIN`, `SQR`, `TAN`. -* Program manipulation: `DISASM`, `EDIT`, `KILL`, `LIST`, `LOAD`,`NEW`, `RUN`, +* Program manipulation: `DISASM`, `EDIT`, `LIST`, `LOAD`,`NEW`, `RUN`, `SAVE`. * Strings and characters: `ASC`, `CHR`, `LEFT`, `LEN`, `LTRIM`, `MID`, `RIGHT`, `RTRIM`, `STR`. diff --git a/std/src/program.rs b/std/src/program.rs index 2e702c5d..b32df942 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -193,64 +193,6 @@ impl Callable for DisasmCommand { } } -/// The `KILL` command. -// TODO(jmmv): This should be in the storage module because it isn't really tied to the stored -// program. However, this currently relies on the automatic addition of extensions to file names, -// which is logic that should only exist here. Maybe we should remove that from this command. -pub struct KillCommand { - metadata: CallableMetadata, - storage: Rc>, -} - -impl KillCommand { - /// Creates a new `KILL` command that deletes a file from `storage`. - pub fn new(storage: Rc>) -> Rc { - Rc::from(Self { - metadata: CallableMetadataBuilder::new("KILL") - .with_syntax(&[( - &[SingularArgSyntax::RequiredValue( - RequiredValueSyntax { - name: Cow::Borrowed("filename"), - vtype: ExprType::Text, - }, - ArgSepSyntax::End, - )], - None, - )]) - .with_category(CATEGORY) - .with_description( - "Deletes the given program. -The filename must be a string and must be a valid EndBASIC path. The .BAS extension is optional \ -but, if present, it must be .BAS. -See the \"File system\" help topic for information on the path syntax.", - ) - .build(), - storage, - }) - } -} - -#[async_trait(?Send)] -impl Callable for KillCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata - } - - async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { - debug_assert_eq!(1, scope.nargs()); - let name = scope.pop_string(); - - let full_name = self - .storage - .borrow() - .make_canonical_with_extension(&name, DEFAULT_EXTENSION) - .map_err(|e| scope.io_error(e))?; - self.storage.borrow_mut().delete(&full_name).await.map_err(|e| scope.io_error(e))?; - - Ok(()) - } -} - /// The `EDIT` command. pub struct EditCommand { metadata: CallableMetadata, @@ -628,7 +570,6 @@ pub fn add_all( ) { machine.add_callable(DisasmCommand::new(console.clone(), program.clone())); machine.add_callable(EditCommand::new(console.clone(), program.clone())); - machine.add_callable(KillCommand::new(storage.clone())); machine.add_callable(ListCommand::new(console.clone(), program.clone())); machine.add_callable(LoadCommand::new(console.clone(), storage.clone(), program.clone())); machine.add_callable(NewCommand::new(console.clone(), program.clone())); @@ -647,39 +588,6 @@ mod tests { const YES_ANSWERS: &[&str] = &["y\n", "yes\n", "Y\n", "YES\n", "true\n", "TRUE\n"]; - #[test] - fn test_kill_ok() { - for p in &["foo", "foo.bas"] { - Tester::default() - .set_program(Some("foo.bas"), "Leave me alone") - .write_file("bar.bas", "") - .write_file("foo.bas", "line 1\n line 2\n") - .run(format!(r#"KILL "{}""#, p)) - .expect_program(Some("foo.bas"), "Leave me alone") - .expect_file("MEMORY:/bar.bas", "") - .check(); - } - } - - #[test] - fn test_kill_errors() { - check_load_save_common_errors("KILL"); - - Tester::default() - .run("KILL") - .expect_compilation_err("1:1: KILL expected filename$") - .check(); - - check_stmt_err("1:1: Entry not found", r#"KILL "missing-file""#); - - Tester::default() - .write_file("mismatched-extension.bat", "") - .run(r#"KILL "mismatched-extension""#) - .expect_err("1:1: Entry not found") - .expect_file("MEMORY:/mismatched-extension.bat", "") - .check(); - } - #[test] fn test_disasm_nothing() { Tester::default().run("DISASM").expect_prints([""]).check(); diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index 32e51284..c8899462 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -221,6 +221,55 @@ impl Callable for DirCommand { } } +/// The `KILL` command. +pub struct KillCommand { + metadata: CallableMetadata, + storage: Rc>, +} + +impl KillCommand { + /// Creates a new `KILL` command that deletes a file from `storage`. + pub fn new(storage: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("KILL") + .with_syntax(&[( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("filename"), + vtype: ExprType::Text, + }, + ArgSepSyntax::End, + )], + None, + )]) + .with_category(CATEGORY) + .with_description( + "Deletes the given file. +The filename must be a string and must be a valid EndBASIC path. +See the \"File system\" help topic for information on the path syntax.", + ) + .build(), + storage, + }) + } +} + +#[async_trait(?Send)] +impl Callable for KillCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + debug_assert_eq!(1, scope.nargs()); + let name = scope.pop_string(); + + self.storage.borrow_mut().delete(&name).await.map_err(|e| scope.io_error(e))?; + + Ok(()) + } +} + /// The `MOUNT` command. pub struct MountCommand { metadata: CallableMetadata, @@ -404,6 +453,7 @@ pub fn add_all( ) { machine.add_callable(CdCommand::new(storage.clone())); machine.add_callable(DirCommand::new(console.clone(), storage.clone())); + machine.add_callable(KillCommand::new(storage.clone())); machine.add_callable(MountCommand::new(console.clone(), storage.clone())); machine.add_callable(PwdCommand::new(console.clone(), storage.clone())); machine.add_callable(UnmountCommand::new(storage)); @@ -648,6 +698,52 @@ mod tests { check_stmt_compilation_err("1:5: expected STRING but found INTEGER", "DIR 2"); } + #[test] + fn test_kill_ok() { + for p in &["foo", "foo.bas"] { + Tester::default() + .set_program(Some(p), "Leave me alone") + .write_file("leave-me-alone.bas", "") + .write_file(p, "line 1\n line 2\n") + .run(format!(r#"KILL "{}""#, p)) + .expect_program(Some(*p), "Leave me alone") + .expect_file("MEMORY:/leave-me-alone.bas", "") + .check(); + } + } + + #[test] + fn test_kill_errors() { + Tester::default() + .run("KILL 3") + .expect_compilation_err("1:6: expected STRING but found INTEGER") + .check(); + + Tester::default() + .run(r#"KILL "a/b.bas""#) + .expect_err("1:1: Too many / separators in path 'a/b.bas'") + .check(); + + Tester::default() + .run(r#"KILL "drive:""#) + .expect_err("1:1: Missing file name in path 'drive:'") + .check(); + + Tester::default() + .run("KILL") + .expect_compilation_err("1:1: KILL expected filename$") + .check(); + + check_stmt_err("1:1: Entry not found", r#"KILL "missing-file""#); + + Tester::default() + .write_file("no-automatic-extension.bas", "") + .run(r#"KILL "no-automatic-extension""#) + .expect_err("1:1: Entry not found") + .expect_file("MEMORY:/no-automatic-extension.bas", "") + .check(); + } + #[test] fn test_mount_list() { let mut t = Tester::default(); From 9dbb26d9f05317fbc43389e7cdd87e947c86c2e2 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 28 Apr 2025 19:06:22 -0700 Subject: [PATCH 050/110] Add the COPY command Using LOAD/SAVE to copy files has been OK for sources, but it does not work for binary files and... well, the workflow was pretty annoying anyway. Introduce a new command dedicated to file copies to make this easier and more flexible. --- NEWS.md | 4 ++ cli/tests/repl/help.bas | 1 + cli/tests/repl/help.out | 12 ++++ std/README.md | 2 +- std/src/storage/cmds.rs | 136 ++++++++++++++++++++++++++++++++++++++++ std/src/storage/mod.rs | 86 ++++++++++++++++++++++--- 6 files changed, 232 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index ba309e7a..27005f00 100644 --- a/NEWS.md +++ b/NEWS.md @@ -45,6 +45,10 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Changed the `KILL` command so that it no longer automatically appends a `.BAS` extension to filenames given to it. This allow deleting files that don't have an extension. + +* Added a new `COPY` command to copy files. The previous approach of `LOAD` + followed by `SAVE` doesn't work for binary files, and we want to support + binary files to handle e.g. images. ## Changes in version 0.11.1 diff --git a/cli/tests/repl/help.bas b/cli/tests/repl/help.bas index 8bb20bb2..9e886e55 100644 --- a/cli/tests/repl/help.bas +++ b/cli/tests/repl/help.bas @@ -64,6 +64,7 @@ DATA "CD" DATA "CLEAR" DATA "CLS" DATA "COLOR" +DATA "COPY" DATA "DEG" DATA "DIR" DATA "DISASM" diff --git a/cli/tests/repl/help.out b/cli/tests/repl/help.out index 2d1dec5c..889d9728 100644 --- a/cli/tests/repl/help.out +++ b/cli/tests/repl/help.out @@ -145,6 +145,7 @@ Output from HELP "FILE SYSTEM": modify, and save programs. >> CD  Changes the current path. + >> COPY  Copies src to dest. >> DIR  Displays the list of files on the current or given path. >> KILL  Deletes the given file. >> MOUNT  Lists the mounted drives or mounts a new drive. @@ -664,6 +665,17 @@ Output from HELP "COLOR": other color specifiable in the 0 to 255 range, as it might be transparent. +Output from HELP "COPY": + + COPY src$, dest$ + + Copies src to dest. + + If dest is a path without a name, the target file given in dest will + have the same name as the source file in src. + + See the "File system" help topic for information on the path syntax. + Output from HELP "DEG":  DEG diff --git a/std/README.md b/std/README.md index cd2c211c..e974d186 100644 --- a/std/README.md +++ b/std/README.md @@ -40,7 +40,7 @@ not intend to be fully compatible with them. The library currently contains: * Graphics: `GFX_CIRCLE`, `GFX_CIRCLEF`, `GFX_HEIGHT`, `GFX_LINE`, `GFX_PIXEL`, `GFX_RECT`, `GFX_RECTF`, `GFX_SYNC`, `GFX_WIDTH`. * Hardware interaction: `GPIO_CLEAR`, `GPIO_READ`, `GPIO_SETUP`, `GPIO_WRITE`. -* File system interaction: `CD`, `DIR`, `KILL`, `MOUNT`, `PWD`, `UNMOUNT`. +* File system interaction: `CD`, `COPY`, `DIR`, `KILL`, `MOUNT`, `PWD`, `UNMOUNT`. * Interpreter interaction: `CLEAR`, `ERRMSG`, `HELP`. * Numerics: `ATN`, `CINT`, `COS`, `DEG`, `INT`, `MAX`, `MIN`, `PI`, `RAD`, `RANDOMIZE`, `RND`, `SIN`, `SQR`, `TAN`. diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index c8899462..59808c4d 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -165,6 +165,67 @@ impl Callable for CdCommand { } } +/// The `COPY` command. +pub struct CopyCommand { + metadata: CallableMetadata, + storage: Rc>, +} + +impl CopyCommand { + /// Creates a new `COPY` command that copies a file. + pub fn new(storage: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("COPY") + .with_syntax(&[( + &[ + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("src"), + vtype: ExprType::Text, + }, + ArgSepSyntax::Exactly(ArgSep::Long), + ), + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("dest"), + vtype: ExprType::Text, + }, + ArgSepSyntax::End, + ), + ], + None, + )]) + .with_category(CATEGORY) + .with_description( + "Copies src to dest. +If dest is a path without a name, the target file given in dest will have the same name \ +as the source file in src. +See the \"File system\" help topic for information on the path syntax.", + ) + .build(), + storage, + }) + } +} + +#[async_trait(?Send)] +impl Callable for CopyCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + debug_assert_eq!(2, scope.nargs()); + let src = scope.pop_string(); + let dest = scope.pop_string(); + + let mut storage = self.storage.borrow_mut(); + storage.copy(&src, &dest).await.map_err(|e| scope.io_error(e))?; + + Ok(()) + } +} + /// The `DIR` command. pub struct DirCommand { metadata: CallableMetadata, @@ -452,6 +513,7 @@ pub fn add_all( storage: Rc>, ) { machine.add_callable(CdCommand::new(storage.clone())); + machine.add_callable(CopyCommand::new(storage.clone())); machine.add_callable(DirCommand::new(console.clone(), storage.clone())); machine.add_callable(KillCommand::new(storage.clone())); machine.add_callable(MountCommand::new(console.clone(), storage.clone())); @@ -485,6 +547,80 @@ mod tests { check_stmt_compilation_err("1:4: expected STRING but found INTEGER", "CD 2"); } + #[test] + fn test_copy_ok() { + Tester::default() + .set_program(Some("foo.bas"), "Leave me alone") + .write_file("file1", "the content") + .run(r#"COPY "file1", "file2""#) + .expect_program(Some("foo.bas"), "Leave me alone") + .expect_file("MEMORY:/file1", "the content") + .expect_file("MEMORY:/file2", "the content") + .check(); + } + + #[test] + fn test_copy_deduce_target_name() { + let t = Tester::default(); + t.get_storage().borrow_mut().mount("other", "memory://").unwrap(); + t.set_program(Some("foo.bas"), "Leave me alone") + .write_file("file1.x", "the content") + .run(r#"COPY "file1.x", "OTHER:/""#) + .expect_program(Some("foo.bas"), "Leave me alone") + .expect_file("MEMORY:/file1.x", "the content") + .expect_file("OTHER:/file1.x", "the content") + .check(); + } + + #[test] + fn test_copy_errors() { + Tester::default() + .run(r#"COPY "foo""#) + .expect_compilation_err("1:1: COPY expected src$, dest$") + .check(); + + Tester::default() + .run(r#"COPY "memory:/", "foo.bar""#) + .expect_err("1:1: Missing file name in copy source path 'memory:/'") + .check(); + + Tester::default() + .run(r#"COPY "missing.txt", "new.txt""#) + .expect_err("1:1: Entry not found") + .check(); + + Tester::default() + .write_file("foo", "irrelevant") + .run(r#"COPY "foo", "missing:/""#) + .expect_err("1:1: Drive 'MISSING' is not mounted") + .expect_file("MEMORY:/foo", "irrelevant") + .check(); + + //Tester::default() + // .run(r#"KILL "a/b.bas""#) + // .expect_err("1:1: Too many / separators in path 'a/b.bas'") + // .check(); + + //Tester::default() + // .run(r#"KILL "drive:""#) + // .expect_err("1:1: Missing file name in path 'drive:'") + // .check(); + + //Tester::default() + // .run("KILL") + // .expect_compilation_err("1:1: KILL expected filename$") + // .check(); + + //check_stmt_err("1:1: Entry not found", r#"KILL "missing-file""#); + + //Tester::default() + // .write_file("no-automatic-extension.bas", "") + // .run(r#"KILL "no-automatic-extension""#) + // .expect_err("1:1: Entry not found") + // .expect_file("MEMORY:/no-automatic-extension.bas", "") + // .check(); + } + #[test] fn test_dir_current_empty() { Tester::default() diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index 8999af16..9d392f55 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -281,6 +281,18 @@ impl Location { } } + /// Sets the leaf name of this path. + fn set_leaf_name(&mut self, name: &str) { + if self.path.starts_with('/') { + self.path.clear(); + self.path.push('/'); + self.path.push_str(name); + } else { + self.path.clear(); + self.path.push_str(name); + } + } + /// Sets the file name extension as long as this location corresponds to a file and not a /// directory and does not already have one. /// @@ -588,11 +600,11 @@ impl Storage { } } - /// Loads the contents of the program given by `raw_location`. - pub async fn get(&self, raw_location: &str) -> io::Result> { - let location = Location::new(raw_location)?; + /// Loads the contents of the program given by `location`. `raw_location` is the + /// string that the user provided and is used for error reporting. + async fn get_location(&self, raw_location: &str, location: &Location) -> io::Result> { match location.leaf_name() { - Some(name) => self.get_drive(&location)?.get(name).await, + Some(name) => self.get_drive(location)?.get(name).await, None => Err(io::Error::new( io::ErrorKind::NotFound, format!("Missing file name in path '{}'", raw_location), @@ -600,6 +612,12 @@ impl Storage { } } + /// Loads the contents of the program given by `raw_location`. + pub async fn get(&self, raw_location: &str) -> io::Result> { + let location = Location::new(raw_location)?; + self.get_location(raw_location, &location).await + } + /// Gets the ACLs of the file `raw_location`. pub async fn get_acls(&self, raw_location: &str) -> io::Result { let location = Location::new(raw_location)?; @@ -612,11 +630,16 @@ impl Storage { } } - /// Saves the in-memory program given by `content` into `raw_location`. - pub async fn put(&mut self, raw_location: &str, content: &[u8]) -> io::Result<()> { - let location = Location::new(raw_location)?; + /// Saves the in-memory program given by `content` into `location`. `raw_location` is the + /// string that the user provided and is used for error reporting. + async fn put_location( + &mut self, + raw_location: &str, + location: &Location, + content: &[u8], + ) -> io::Result<()> { match location.leaf_name() { - Some(name) => self.get_drive_mut(&location)?.put(name, content).await, + Some(name) => self.get_drive_mut(location)?.put(name, content).await, None => Err(io::Error::new( io::ErrorKind::NotFound, format!("Missing file name in path '{}'", raw_location), @@ -624,6 +647,12 @@ impl Storage { } } + /// Saves the in-memory program given by `content` into `raw_location`. + pub async fn put(&mut self, raw_location: &str, content: &[u8]) -> io::Result<()> { + let location = Location::new(raw_location)?; + self.put_location(raw_location, &location, content).await + } + /// Updates the ACLs of the file `raw_location` by extending them with the contents of `add` and /// removing the existing entries listed in `remove`. pub async fn update_acls( @@ -650,6 +679,28 @@ impl Storage { None => Ok(self.get_drive(&location)?.system_path("")), } } + + /// Copies file `src` to `dest`. + pub async fn copy(&mut self, raw_src: &str, raw_dest: &str) -> io::Result<()> { + let src = Location::new(raw_src)?; + let src_name = match src.leaf_name() { + Some(name) => name, + None => { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("Missing file name in copy source path '{}'", raw_src), + )); + } + }; + + let mut dest = Location::new(raw_dest)?; + if dest.leaf_name().is_none() { + dest.set_leaf_name(src_name); + } + + let content = self.get_location(raw_src, &src).await?; + self.put_location(raw_dest, &dest, &content).await + } } #[cfg(test)] @@ -746,6 +797,25 @@ mod tests { assert_eq!(Some("abc.txt"), Location::new("abc.txt").unwrap().leaf_name()); } + #[test] + fn test_location_set_leaf_name() { + let mut location = Location::new("drv:/").unwrap(); + location.set_leaf_name("foo"); + assert_eq!("DRV:/foo", format!("{}", location)); + + let mut location = Location::new("drv:").unwrap(); + location.set_leaf_name("foo"); + assert_eq!("DRV:/foo", format!("{}", location)); + + let mut location = Location::new("/bar").unwrap(); + location.set_leaf_name("foo"); + assert_eq!("/foo", format!("{}", location)); + + let mut location = Location::new("bar").unwrap(); + location.set_leaf_name("foo"); + assert_eq!("foo", format!("{}", location)); + } + #[test] fn test_location_set_extension() { for (exp_location, raw_location, extension) in [ From 326ebc6126aeb355daeba8edc6d1db8b3f3ac27d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 4 May 2025 15:01:46 -0700 Subject: [PATCH 051/110] Fix SDL test interference with SDL2-compat Fedora 42 ships with SDL3 and the SDL2-compat package to offer backwards compatibility for SDL2 apps like EndBASIC: there is no way to use the real SDL2 library. And, with SDL2-compat, I'm seeing that all SDL tests fail except when run in isolation. (I'm not even sure that SDL2-compat is at fault here.) I don't quite know what the problem is, but there seems to be some sort of state pollution when loading the golden BPM of the test _before_ capturing the actual contents of the surface. Reordering these operations fixes the majority of the tests except one that's really broken with the SDL2-compat upgrade. --- sdl/src/console.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdl/src/console.rs b/sdl/src/console.rs index e9ca740e..09d3d992 100644 --- a/sdl/src/console.rs +++ b/sdl/src/console.rs @@ -385,12 +385,8 @@ mod testutils { panic!("Golden data regenerated; flip REGEN_BMPS back to false"); } - let golden = { - let input = BufReader::new(File::open(golden_bmp_gz).unwrap()); - let mut decoder = GzDecoder::new(input); - let mut buffer = vec![]; - decoder.read_to_end(&mut buffer).unwrap(); - let mut rwops = RWops::from_bytes(buffer.as_slice()).unwrap(); + let actual = { + let mut rwops = RWops::from_file(actual_bmp, "r").unwrap(); Surface::load_bmp_rw(&mut rwops) .unwrap() .into_canvas() @@ -399,8 +395,12 @@ mod testutils { .unwrap() }; - let actual = { - let mut rwops = RWops::from_file(actual_bmp, "r").unwrap(); + let golden = { + let input = BufReader::new(File::open(golden_bmp_gz).unwrap()); + let mut decoder = GzDecoder::new(input); + let mut buffer = vec![]; + decoder.read_to_end(&mut buffer).unwrap(); + let mut rwops = RWops::from_bytes(buffer.as_slice()).unwrap(); Surface::load_bmp_rw(&mut rwops) .unwrap() .into_canvas() From 9a7c3621fc9e00ae81d66ae0fcf0809da488b63f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 4 May 2025 15:45:29 -0700 Subject: [PATCH 052/110] Fix SDL read_pixels ops outside of the viewport SDL2-compat does not like it when we try to call read_pixels on an area that doesn't intersect the viewport -- but SDL2 seemed to cope with this just fine. Mitigate this difference so that our code continues to work with SDL3 via SDL2-compat. The specific problem that this fixes is that the write_positions test crashed when trying to write characters off screen, because capturing the text under the cursor-to-be-drawn failed per the above. --- sdl/src/host.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/sdl/src/host.rs b/sdl/src/host.rs index 0aca7b09..631b5115 100644 --- a/sdl/src/host.rs +++ b/sdl/src/host.rs @@ -340,8 +340,23 @@ impl RasterOps for Context { fn read_pixels(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result { let rect = rect_origin_size(xy, size); - let data = - self.canvas.read_pixels(rect, self.pixel_format).map_err(string_error_to_io_error)?; + let data = match self.canvas.read_pixels(rect, self.pixel_format) { + Ok(data) => data, + Err(e) if e == "Can't read outside the current viewport" => { + // The SDL read pixels operation intersects the requested rect with the viewport. + // However, SDL2-compat does not like it when the two don't intersect at all. + // Fake the response so that our SDL2 code remains compatible with SDL2-compat. + vec![ + 0; + usize::from(size.width) + * usize::from(size.height) + * self.pixel_format.byte_size_per_pixel() + ] + } + Err(e) => { + return Err(string_error_to_io_error(e)); + } + }; Ok((data, size)) } From 49a5f9dc0b4907c13062b98e669ab65d36837204 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 4 May 2025 16:38:41 -0700 Subject: [PATCH 053/110] Fix rendering of zero-sized rectangles --- NEWS.md | 3 ++ sdl/src/console.rs | 39 ++++++++++++++++ sdl/src/sdl-draw-one-sized-rectangles.bmp.gz | Bin 0 -> 1555 bytes std/src/console/graphics.rs | 45 ++++++++++++------- 4 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 sdl/src/sdl-draw-one-sized-rectangles.bmp.gz diff --git a/NEWS.md b/NEWS.md index 27005f00..f38d07c9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -49,6 +49,9 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Added a new `COPY` command to copy files. The previous approach of `LOAD` followed by `SAVE` doesn't work for binary files, and we want to support binary files to handle e.g. images. + +* Fixed the rendering of zero-sized rectangles in the SDL console so that + debug builds don't crash and so that the behavior matches the web console. ## Changes in version 0.11.1 diff --git a/sdl/src/console.rs b/sdl/src/console.rs index 09d3d992..4363e4b1 100644 --- a/sdl/src/console.rs +++ b/sdl/src/console.rs @@ -819,6 +819,45 @@ mod tests { test.verify("sdl-draw"); } + #[test] + #[ignore = "Requires a graphical environment"] + fn test_sdl_console_draw_zero_sized_shapes() { + let mut test = SdlTest::new(); + let console = test.console(); + + fn xy(x: i16, y: i16) -> PixelsXY { + PixelsXY { x, y } + } + + console.set_color(Some(15), None).unwrap(); + console.draw_line(xy(10, 10), xy(10, 10)).unwrap(); + console.draw_rect(xy(20, 10), xy(20, 10)).unwrap(); + console.draw_rect_filled(xy(30, 10), xy(30, 10)).unwrap(); + console.draw_circle(xy(40, 40), 0).unwrap(); + console.draw_circle_filled(xy(50, 50), 0).unwrap(); + + test.verify("sdl-empty"); + } + + #[test] + #[ignore = "Requires a graphical environment"] + fn test_sdl_console_draw_one_sized_rectangles() { + let mut test = SdlTest::new(); + let console = test.console(); + + fn xy(x: i16, y: i16) -> PixelsXY { + PixelsXY { x, y } + } + + console.set_color(Some(15), None).unwrap(); + console.draw_rect(xy(10, 10), xy(10, 15)).unwrap(); + console.draw_rect(xy(20, 20), xy(25, 20)).unwrap(); + console.draw_rect_filled(xy(30, 30), xy(30, 35)).unwrap(); + console.draw_rect_filled(xy(40, 40), xy(45, 40)).unwrap(); + + test.verify("sdl-draw-one-sized-rectangles"); + } + #[test] #[ignore = "Requires a graphical environment"] fn test_sdl_console_show_cursor() { diff --git a/sdl/src/sdl-draw-one-sized-rectangles.bmp.gz b/sdl/src/sdl-draw-one-sized-rectangles.bmp.gz new file mode 100644 index 0000000000000000000000000000000000000000..3c6bb3288482189ae395bb9368646d63a87e089b GIT binary patch literal 1555 zcmb2|=3oGW|8H*@avgFIXuBAAULhi?s!8U+y?c+VbXQ9p%}Z4L!OdC8zwBn}uk|xt z=T^wQ&Ykhh=&QoFWAJ# zFk$I>28K;vL>U~W?PFp%aVeaEA@Ua&LKQMAV!C^HeR+-u!+~diK?=8^YWusd2xy?e rbYycByyF=eQeJ8^G_0wM7iC~D1~GMNS#jBmq+a}1V|AI?Pf-Q{a79`& literal 0 HcmV?d00001 diff --git a/std/src/console/graphics.rs b/std/src/console/graphics.rs index e95fad0e..69f7e01c 100644 --- a/std/src/console/graphics.rs +++ b/std/src/console/graphics.rs @@ -150,7 +150,7 @@ impl ClampedMul for CharsXY { } /// Given two points, calculates the origin and size of the rectangle they define. -fn rect_points(x1y1: PixelsXY, x2y2: PixelsXY) -> (PixelsXY, SizeInPixels) { +fn rect_points(x1y1: PixelsXY, x2y2: PixelsXY) -> Option<(PixelsXY, SizeInPixels)> { let (x1, x2) = if x1y1.x < x2y2.x { (x1y1.x, x2y2.x) } else { (x2y2.x, x1y1.x) }; let (y1, y2) = if x1y1.y < x2y2.y { (x1y1.y, x2y2.y) } else { (x2y2.y, x1y1.y) }; @@ -173,7 +173,11 @@ fn rect_points(x1y1: PixelsXY, x2y2: PixelsXY) -> (PixelsXY, SizeInPixels) { } .clamped_into(); - (PixelsXY::new(x1, y1), SizeInPixels::new(width, height)) + if width == 0 || height == 0 { + None + } else { + Some((PixelsXY::new(x1, y1), SizeInPixels::new(width, height))) + } } /// Container for configuration information of the backing surface. @@ -688,16 +692,20 @@ where } fn draw_rect(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> { - let (xy, size) = rect_points(x1y1, x2y2); self.raster_ops.set_draw_color(self.fg_color); - self.raster_ops.draw_rect(xy, size)?; + match rect_points(x1y1, x2y2) { + Some((xy, size)) => self.raster_ops.draw_rect(xy, size)?, + None => self.raster_ops.draw_line(x1y1, x2y2)?, + } self.present_canvas() } fn draw_rect_filled(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> { - let (xy, size) = rect_points(x1y1, x2y2); self.raster_ops.set_draw_color(self.fg_color); - self.raster_ops.draw_rect_filled(xy, size)?; + match rect_points(x1y1, x2y2) { + Some((xy, size)) => self.raster_ops.draw_rect_filled(xy, size)?, + None => self.raster_ops.draw_line(x1y1, x2y2)?, + } self.present_canvas() } @@ -824,40 +832,47 @@ mod tests { } #[test] - fn test_rect_points() { + fn test_rect_points_ok() { assert_eq!( - (PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200)), + Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))), rect_points(PixelsXY { x: 10, y: 20 }, PixelsXY { x: 110, y: 220 }) ); assert_eq!( - (PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200)), + Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))), rect_points(PixelsXY { x: 110, y: 20 }, PixelsXY { x: 10, y: 220 }) ); assert_eq!( - (PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200)), + Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))), rect_points(PixelsXY { x: 10, y: 220 }, PixelsXY { x: 110, y: 20 }) ); assert_eq!( - (PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200)), + Some((PixelsXY { x: 10, y: 20 }, SizeInPixels::new(100, 200))), rect_points(PixelsXY { x: 110, y: 220 }, PixelsXY { x: 10, y: 20 }) ); assert_eq!( - (PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(31005, 32010)), + Some((PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(31005, 32010))), rect_points(PixelsXY { x: 5, y: -32000 }, PixelsXY { x: -31000, y: 10 }) ); assert_eq!( - (PixelsXY { x: 10, y: 5 }, SizeInPixels::new(30990, 31995)), + Some((PixelsXY { x: 10, y: 5 }, SizeInPixels::new(30990, 31995))), rect_points(PixelsXY { x: 31000, y: 5 }, PixelsXY { x: 10, y: 32000 }) ); assert_eq!( - (PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(62000, 64000)), + Some((PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(62000, 64000))), rect_points(PixelsXY { x: -31000, y: -32000 }, PixelsXY { x: 31000, y: 32000 }) ); assert_eq!( - (PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(62000, 64000)), + Some((PixelsXY { x: -31000, y: -32000 }, SizeInPixels::new(62000, 64000))), rect_points(PixelsXY { x: 31000, y: 32000 }, PixelsXY { x: -31000, y: -32000 }) ); } + + #[test] + fn test_rect_points_zeroes() { + assert_eq!(None, rect_points(PixelsXY { x: 10, y: 10 }, PixelsXY { x: 10, y: 10 })); + assert_eq!(None, rect_points(PixelsXY { x: 10, y: 10 }, PixelsXY { x: 10, y: 20 })); + assert_eq!(None, rect_points(PixelsXY { x: 10, y: 10 }, PixelsXY { x: 20, y: 10 })); + } } From b122c0c2c1d4c55ffcd99122fd745b2ad7bc718f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 8 May 2025 20:22:06 -0700 Subject: [PATCH 054/110] Use the new headers-based "patch file" cloud API This avoids the overhead of wrapping the file content into a JSON message and base64-encoding it, which was never a great design in the first place. I was hoping this would let me drop the base64 dependency, but we still need it to encode the auth headers. --- client/src/cloud.rs | 110 +++++++++++++++++++++++++++++----------- client/src/drive.rs | 55 +++++++++++--------- client/src/lib.rs | 52 +++++-------------- client/src/testutils.rs | 70 +++++++++++++++++++------ 4 files changed, 180 insertions(+), 107 deletions(-) diff --git a/client/src/cloud.rs b/client/src/cloud.rs index ba895c72..722d0f04 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -17,6 +17,7 @@ use crate::*; use async_trait::async_trait; +use base64::prelude::*; use bytes::Buf; use endbasic_std::console::remove_control_chars; use endbasic_std::storage::FileAcls; @@ -302,11 +303,11 @@ impl Service for CloudService { } } - async fn patch_file( + async fn patch_file_content( &mut self, username: &str, filename: &str, - request: &PatchFileRequest, + content: Vec, ) -> io::Result<()> { let auth_data = self.auth_data.borrow(); @@ -314,8 +315,44 @@ impl Service for CloudService { .client .patch(self.make_url(&format!("api/users/{}/files/{}", username, filename))) .headers(self.default_headers()) - .header("Content-Type", "application/json") - .body(serde_json::to_vec(&request)?) + .header("Content-Type", "application/octet-stream") + .header("X-EndBASIC-PatchContent", "true") + .body(content) + .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str()) + .send() + .await + .map_err(reqwest_error_to_io_error)?; + match response.status() { + StatusCode::OK | StatusCode::CREATED => Ok(()), + _ => Err(http_response_to_io_error(response).await), + } + } + + async fn patch_file_acls( + &mut self, + username: &str, + filename: &str, + add: &FileAcls, + remove: &FileAcls, + ) -> io::Result<()> { + let auth_data = self.auth_data.borrow(); + + let mut builder = self + .client + .patch(self.make_url(&format!("api/users/{}/files/{}", username, filename))) + .headers(self.default_headers()) + .header("Content-Type", "application/octet-stream") + // Ensure we have at least one header to go through the header-based request handler. + .header("X-EndBASIC-PatchContent", "false"); + + for reader in add.readers() { + builder = builder.header("X-EndBASIC-AddReader", reader); + } + for reader in remove.readers() { + builder = builder.header("X-EndBASIC-RemoveReader", reader); + } + + let response = builder .bearer_auth(Self::require_auth_data(auth_data.as_ref())?.access_token.as_str()) .send() .await @@ -403,11 +440,11 @@ mod testutils { /// Generates a random filename and its content for testing, and makes sure the file gets /// deleted during cleanup in case the test didn't do it on its own. - pub(crate) fn random_file(&mut self) -> (String, String) { + pub(crate) fn random_file(&mut self) -> (String, Vec) { let filename = format!("file-{}", rand::random::()); let content = format!("Test content for {}", filename); self.files_to_delete.push(filename.clone()); - (filename, content) + (filename, content.into_bytes()) } } @@ -492,7 +529,7 @@ mod tests { for _ in 0..5 { let (filename, content) = context.random_file(); - needed_bytes += content.as_bytes().len() as u64; + needed_bytes += content.len() as u64; needed_files += 1; filenames_and_contents.push((filename, content)); } @@ -514,8 +551,7 @@ mod tests { } for (filename, content) in &filenames_and_contents { - let request = PatchFileRequest::default().with_content(content.as_bytes()); - service.patch_file(&username, filename, &request).await.unwrap(); + service.patch_file_content(&username, filename, content.clone()).await.unwrap(); } let response = service.get_files(&username).await.unwrap(); @@ -526,13 +562,16 @@ mod tests { run(&mut TestContext::new_from_env()); } - async fn do_get_and_patch_file_test(context: &mut TestContext, filename: &str, content: &[u8]) { + async fn do_get_and_patch_file_test>>( + context: &mut TestContext, + filename: &str, + content: B, + ) { let username = context.do_login(1).await; let mut service = context.service(); - let request = PatchFileRequest::default().with_content(content); - service.patch_file(&username, filename, &request).await.unwrap(); - + let content = content.into(); + service.patch_file_content(&username, filename, content.clone()).await.unwrap(); assert_eq!(content, service.get_file(&username, filename).await.unwrap()); } @@ -542,7 +581,7 @@ mod tests { #[tokio::main] async fn run(context: &mut TestContext) { let (filename, content) = context.random_file(); - do_get_and_patch_file_test(context, &filename, content.as_bytes()).await; + do_get_and_patch_file_test(context, &filename, content).await; } run(&mut TestContext::new_from_env()); } @@ -565,7 +604,7 @@ mod tests { async fn run(context: &mut TestContext) { let (filename, _content) = context.random_file(); let content = "안녕하세요"; - do_get_and_patch_file_test(context, &filename, content.as_bytes()).await; + do_get_and_patch_file_test(context, &filename, content).await; } run(&mut TestContext::new_from_env()); } @@ -597,8 +636,10 @@ mod tests { let (filename, _content) = context.random_file(); context.do_logout().await; - let request = PatchFileRequest::default().with_content("foo"); - let err = service.patch_file(&username, &filename, &request).await.unwrap_err(); + let err = service + .patch_file_content(&username, &filename, b"foo".to_vec()) + .await + .unwrap_err(); assert_eq!(io::ErrorKind::PermissionDenied, err.kind(), "{}", err); assert!(format!("{}", err).contains("Not logged in")); } @@ -618,9 +659,8 @@ mod tests { let username2 = context.get_username(2); // Share username1's file with username2. - let request = PatchFileRequest::default().with_content(content.clone()); context.do_login(1).await; - service.patch_file(&username1, &filename, &request).await.unwrap(); + service.patch_file_content(&username1, &filename, content.clone()).await.unwrap(); // Read username1's file as username2 before it is shared. context.do_login(2).await; @@ -629,13 +669,20 @@ mod tests { // Share username1's file with username2. context.do_login(1).await; - let request = PatchFileRequest::default().with_add_readers([username2]); - service.patch_file(&username1, &filename, &request).await.unwrap(); + service + .patch_file_acls( + &username1, + &filename, + &FileAcls::default().with_readers([username2]), + &FileAcls::default(), + ) + .await + .unwrap(); // Read username1's file as username2 again, now that it is shared. context.do_login(2).await; let response = service.get_file(&username1, &filename).await.unwrap(); - assert_eq!(content.as_bytes(), response); + assert_eq!(content, response); } run(&mut TestContext::new_from_env()); } @@ -652,9 +699,8 @@ mod tests { let username1 = context.get_username(1); // Share username1's file with the public. - let request = PatchFileRequest::default().with_content(content.clone()); context.do_login(1).await; - service.patch_file(&username1, &filename, &request).await.unwrap(); + service.patch_file_content(&username1, &filename, content.clone()).await.unwrap(); // Read username1's file as a guest before it is shared. context.do_logout().await; @@ -663,13 +709,20 @@ mod tests { // Share username1's file with the public. context.do_login(1).await; - let request = PatchFileRequest::default().with_add_readers(["public".to_owned()]); - service.patch_file(&username1, &filename, &request).await.unwrap(); + service + .patch_file_acls( + &username1, + &filename, + &FileAcls::default().with_readers(["public".to_owned()]), + &FileAcls::default(), + ) + .await + .unwrap(); // Read username1's file as a guest again, now that it is shared. context.do_logout().await; let response = service.get_file(&username1, &filename).await.unwrap(); - assert_eq!(content.as_bytes(), response); + assert_eq!(content, response); } run(&mut TestContext::new_from_env()); } @@ -683,8 +736,7 @@ mod tests { let mut service = context.service(); let (filename, content) = context.random_file(); - let request = PatchFileRequest::default().with_content(content); - service.patch_file(&username, &filename, &request).await.unwrap(); + service.patch_file_content(&username, &filename, content).await.unwrap(); service.delete_file(&username, &filename).await.unwrap(); diff --git a/client/src/drive.rs b/client/src/drive.rs index e4230607..3deb3b05 100644 --- a/client/src/drive.rs +++ b/client/src/drive.rs @@ -70,8 +70,10 @@ impl Drive for CloudDrive { } async fn put(&mut self, filename: &str, content: &[u8]) -> io::Result<()> { - let request = PatchFileRequest::default().with_content(content); - self.service.borrow_mut().patch_file(&self.username, filename, &request).await + self.service + .borrow_mut() + .patch_file_content(&self.username, filename, content.to_vec()) + .await } async fn update_acls( @@ -80,19 +82,7 @@ impl Drive for CloudDrive { add: &FileAcls, remove: &FileAcls, ) -> io::Result<()> { - let mut request = PatchFileRequest::default(); - - let add = add.readers(); - if !add.is_empty() { - request.add_readers = Some(add.to_vec()); - } - - let remove = remove.readers(); - if !remove.is_empty() { - request.remove_readers = Some(remove.to_vec()); - } - - self.service.borrow_mut().patch_file(&self.username, filename, &request).await + self.service.borrow_mut().patch_file_acls(&self.username, filename, add, remove).await } } @@ -261,8 +251,12 @@ mod tests { service.borrow_mut().do_login().await; let mut drive = CloudDrive::new(service.clone(), "the-user"); - let request = PatchFileRequest::default().with_content("some content"); - service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); + service.borrow_mut().add_mock_patch_file_content( + "the-user", + "the-filename", + "some content", + Ok(()), + ); drive.put("the-filename", b"some content").await.unwrap(); service.take().verify_all_used(); @@ -274,12 +268,20 @@ mod tests { service.borrow_mut().do_login().await; let mut drive = CloudDrive::new(service.clone(), "the-user"); - let request = PatchFileRequest::default().with_content("some content"); - service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); + service.borrow_mut().add_mock_patch_file_content( + "the-user", + "the-filename", + "some content", + Ok(()), + ); drive.put("the-filename", b"some content").await.unwrap(); - let request = PatchFileRequest::default().with_content("some other content"); - service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); + service.borrow_mut().add_mock_patch_file_content( + "the-user", + "the-filename", + "some other content", + Ok(()), + ); drive.put("the-filename", b"some other content").await.unwrap(); service.take().verify_all_used(); @@ -291,10 +293,13 @@ mod tests { service.borrow_mut().do_login().await; let mut drive = CloudDrive::new(service.clone(), "the-user"); - let request = PatchFileRequest::default() - .with_add_readers(["r1".to_owned(), "r2".to_owned()]) - .with_remove_readers(["r2".to_owned(), "r3".to_owned()]); - service.borrow_mut().add_mock_patch_file("the-user", "the-filename", request, Ok(())); + service.borrow_mut().add_mock_patch_file_acls( + "the-user", + "the-filename", + ["r1".to_owned(), "r2".to_owned()], + ["r2".to_owned(), "r3".to_owned()], + Ok(()), + ); drive .update_acls( "the-filename", diff --git a/client/src/lib.rs b/client/src/lib.rs index 89200ae3..c569f7c2 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -23,7 +23,6 @@ #![warn(unsafe_code)] use async_trait::async_trait; -use base64::prelude::*; use endbasic_std::storage::{DiskSpace, FileAcls}; use serde::{Deserialize, Serialize}; use std::io; @@ -112,39 +111,6 @@ pub struct GetFilesResponse { disk_free: Option, } -/// Representation of an atomic file update. -#[derive(Debug, Default, Eq, PartialEq, Serialize)] -#[cfg_attr(test, derive(Deserialize))] -pub struct PatchFileRequest { - /// Base64-encoded file content. - content: Option, - - add_readers: Option>, - remove_readers: Option>, -} - -impl PatchFileRequest { - /// Updates the file's content with `content`. The content is automatically base64-encoded. - fn with_content>(mut self, content: C) -> Self { - self.content = Some(BASE64_STANDARD.encode(content)); - self - } - - /// Adds `readers` to the file's reader ACLs. - #[cfg(test)] - fn with_add_readers>>(mut self, readers: R) -> Self { - self.add_readers = Some(readers.into()); - self - } - - /// Removes `readers` from the file's reader ACLs. - #[cfg(test)] - fn with_remove_readers>>(mut self, readers: R) -> Self { - self.remove_readers = Some(readers.into()); - self - } -} - /// Representation of a signup request. #[derive(Debug, Default, Eq, PartialEq, Serialize)] #[cfg_attr(test, derive(Deserialize))] @@ -188,13 +154,23 @@ pub trait Service { /// previously-acquired `access_token`. async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result; - /// Sends a request to the server to update the metadata and/or the contents of `filename` owned - /// by `username` as specified in `request` with a previously-acquired `access_token`. - async fn patch_file( + /// Sends a request to the server to update the contents of `filename` owned by `username` as + /// specified in `content` with a previously-acquired `access_token`. + async fn patch_file_content( + &mut self, + username: &str, + filename: &str, + content: Vec, + ) -> io::Result<()>; + + /// Sends a request to the server to update the ACLs of `filename` owned by `username` as + /// specified in `add` and `remove` with a previously-acquired `access_token`. + async fn patch_file_acls( &mut self, username: &str, filename: &str, - request: &PatchFileRequest, + add: &FileAcls, + remove: &FileAcls, ) -> io::Result<()>; /// Sends a request to the server to delete `filename` owned by `username` with a diff --git a/client/src/testutils.rs b/client/src/testutils.rs index 4e6debf9..e818a5b1 100644 --- a/client/src/testutils.rs +++ b/client/src/testutils.rs @@ -15,9 +15,7 @@ //! Test utilities for the cloud service. -use crate::{ - add_all, AccessToken, GetFilesResponse, LoginResponse, PatchFileRequest, Service, SignupRequest, -}; +use crate::{add_all, AccessToken, GetFilesResponse, LoginResponse, Service, SignupRequest}; use async_trait::async_trait; use endbasic_std::storage::{FileAcls, Storage}; use endbasic_std::testutils::*; @@ -37,7 +35,8 @@ pub struct MockService { mock_get_files: VecDeque<(String, io::Result)>, mock_get_file: VecDeque<((String, String), io::Result>)>, mock_get_file_acls: VecDeque<((String, String), io::Result)>, - mock_patch_file: VecDeque<((String, String, PatchFileRequest), io::Result<()>)>, + mock_patch_file_content: VecDeque<((String, String, Vec), io::Result<()>)>, + mock_patch_file_acls: VecDeque<((String, String, FileAcls, FileAcls), io::Result<()>)>, mock_delete_file: VecDeque<((String, String), io::Result<()>)>, } @@ -111,18 +110,40 @@ impl MockService { self.mock_get_file_acls.push_back((exp_request, result)); } - /// Records the behavior of an upcoming "patch file" operation for the `username`/`filename` - /// pair with a request that looks like `exp_request` and that returns `result`. + /// Records the behavior of an upcoming "patch file content" operation for the + /// `username`/`filename` pair with `exp_content` and that returns `result`. #[cfg(test)] - pub(crate) fn add_mock_patch_file( + pub(crate) fn add_mock_patch_file_content>>( &mut self, username: &str, filename: &str, - exp_request: PatchFileRequest, + exp_content: B, result: io::Result<()>, ) { - let exp_request = (username.to_owned(), filename.to_owned(), exp_request); - self.mock_patch_file.push_back((exp_request, result)); + let exp_request = (username.to_owned(), filename.to_owned(), exp_content.into()); + self.mock_patch_file_content.push_back((exp_request, result)); + } + + /// Records the behavior of an upcoming "patch file ACLS" operation for the + /// `username`/`filename` pair with `exp_add` and `exp_remove` and that returns `result`. + #[cfg(test)] + pub(crate) fn add_mock_patch_file_acls, V: Into>>( + &mut self, + username: &str, + filename: &str, + exp_add: V, + exp_remove: V, + result: io::Result<()>, + ) { + let exp_add = FileAcls { + readers: exp_add.into().into_iter().map(|v| v.into()).collect::>(), + }; + 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()); + self.mock_patch_file_acls.push_back((exp_request, result)); } /// Records the behavior of an upcoming "delete file" operation for the `username`/`filename` @@ -144,7 +165,9 @@ impl MockService { assert!(self.mock_login.is_empty(), "Mock requests not fully consumed"); assert!(self.mock_get_files.is_empty(), "Mock requests not fully consumed"); assert!(self.mock_get_file.is_empty(), "Mock requests not fully consumed"); - assert!(self.mock_patch_file.is_empty(), "Mock requests not fully consumed"); + assert!(self.mock_get_file_acls.is_empty(), "Mock requests not fully consumed"); + assert!(self.mock_patch_file_content.is_empty(), "Mock requests not fully consumed"); + assert!(self.mock_patch_file_acls.is_empty(), "Mock requests not fully consumed"); assert!(self.mock_delete_file.is_empty(), "Mock requests not fully consumed"); } } @@ -211,18 +234,35 @@ impl Service for MockService { mock.1 } - async fn patch_file( + async fn patch_file_content( + &mut self, + username: &str, + filename: &str, + content: Vec, + ) -> io::Result<()> { + 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); + mock.1 + } + + async fn patch_file_acls( &mut self, username: &str, filename: &str, - request: &PatchFileRequest, + add: &FileAcls, + remove: &FileAcls, ) -> io::Result<()> { self.access_token.as_ref().expect("login not called yet"); - let mock = self.mock_patch_file.pop_front().expect("No mock requests available"); + 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, request); + assert_eq!(&mock.0 .2, add); + assert_eq!(&mock.0 .3, remove); mock.1 } From a2f457515fd4aa0c689e560151be74e14aa6b4a4 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 10 Jun 2025 07:25:31 -0700 Subject: [PATCH 055/110] Appease the new clippy::io_other_error warning --- client/src/cloud.rs | 2 +- core/src/reader.rs | 5 +---- core/src/testutils.rs | 10 ++-------- rpi/src/gpio.rs | 2 +- sdl/src/host.rs | 2 +- sdl/src/lib.rs | 2 +- std/src/console/mod.rs | 14 +++++++------- std/src/console/readline.rs | 5 +---- std/src/gpio/fakes.rs | 10 +++++----- std/src/program.rs | 2 +- std/src/storage/mod.rs | 6 +++--- std/src/testutils.rs | 2 +- web/src/canvas.rs | 4 ++-- web/src/store.rs | 8 ++++---- 14 files changed, 31 insertions(+), 43 deletions(-) diff --git a/client/src/cloud.rs b/client/src/cloud.rs index 722d0f04..ede5eeb7 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -78,7 +78,7 @@ async fn http_response_to_io_error(response: Response) -> io::Error { /// Converts a `reqwest::Error` to an `io::Error`. fn reqwest_error_to_io_error(e: reqwest::Error) -> io::Error { - io::Error::new(io::ErrorKind::Other, format!("{}", e)) + io::Error::other(format!("{}", e)) } /// Container for authentication data to track after login. diff --git a/core/src/reader.rs b/core/src/reader.rs index bfc8e10a..4ba3934e 100644 --- a/core/src/reader.rs +++ b/core/src/reader.rs @@ -160,10 +160,7 @@ impl Iterator for CharReader<'_> { } Pending::Error(e) => match e.take() { Some(e) => Some(Err(e)), - None => Some(Err(io::Error::new( - io::ErrorKind::Other, - "Invalid state; error already consumed", - ))), + None => Some(Err(io::Error::other("Invalid state; error already consumed"))), }, } } diff --git a/core/src/testutils.rs b/core/src/testutils.rs index ab64292c..d15b1a2d 100644 --- a/core/src/testutils.rs +++ b/core/src/testutils.rs @@ -158,10 +158,7 @@ impl Callable for RaisefFunction { "argument" => Err(Error::SyntaxError(pos, "Bad argument".to_owned())), "eval" => Err(Error::EvalError(pos, "Some eval error".to_owned())), "internal" => Err(Error::InternalError(pos, "Some internal error".to_owned())), - "io" => Err(Error::IoError( - pos, - io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()), - )), + "io" => Err(Error::IoError(pos, io::Error::other("Some I/O error".to_owned()))), _ => panic!("Invalid arguments"), } } @@ -201,10 +198,7 @@ impl Callable for RaiseCommand { "argument" => Err(Error::SyntaxError(pos, "Bad argument".to_owned())), "eval" => Err(Error::EvalError(pos, "Some eval error".to_owned())), "internal" => Err(Error::InternalError(pos, "Some internal error".to_owned())), - "io" => Err(Error::IoError( - pos, - io::Error::new(io::ErrorKind::Other, "Some I/O error".to_owned()), - )), + "io" => Err(Error::IoError(pos, io::Error::other("Some I/O error".to_owned()))), _ => panic!("Invalid arguments"), } } diff --git a/rpi/src/gpio.rs b/rpi/src/gpio.rs index 61cf1e26..32f0333e 100644 --- a/rpi/src/gpio.rs +++ b/rpi/src/gpio.rs @@ -39,7 +39,7 @@ pub(crate) fn gpio_error_to_io_error(e: gpio::Error) -> io::Error { gpio::Error::PinNotAvailable(pin) => { io::Error::new(io::ErrorKind::InvalidInput, format!("Unknown pin number {}", pin)) } - e => io::Error::new(io::ErrorKind::Other, e.to_string()), + e => io::Error::other(e.to_string()), } } diff --git a/sdl/src/host.rs b/sdl/src/host.rs index 631b5115..658efdd1 100644 --- a/sdl/src/host.rs +++ b/sdl/src/host.rs @@ -58,7 +58,7 @@ const LOOP_DELAY_MS: u64 = 1; /// Converts a `fmt::Error` to an `io::Error`. fn fmt_error_to_io_error(e: fmt::Error) -> io::Error { - io::Error::new(io::ErrorKind::Other, e) + io::Error::other(e) } /// Converts a `TextureValueError` to an `io::Error`. diff --git a/sdl/src/lib.rs b/sdl/src/lib.rs index a8e050aa..0c40ef59 100644 --- a/sdl/src/lib.rs +++ b/sdl/src/lib.rs @@ -48,7 +48,7 @@ const DEFAULT_FONT_SIZE: u16 = 16; /// Converts a flat string error message to an `io::Error`. fn string_error_to_io_error(e: String) -> io::Error { - io::Error::new(io::ErrorKind::Other, e) + io::Error::other(e) } /// Context to maintain a font on disk temporarily. diff --git a/std/src/console/mod.rs b/std/src/console/mod.rs index 8983fe49..48df5ece 100644 --- a/std/src/console/mod.rs +++ b/std/src/console/mod.rs @@ -248,7 +248,7 @@ pub trait Console { /// Queries the size of the graphical console. fn size_pixels(&self) -> io::Result { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Writes the text into the console at the position of the cursor. @@ -257,32 +257,32 @@ pub trait Console { /// Draws the outline of a circle at `_center` with `_radius` using the current drawing color. fn draw_circle(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Draws a filled circle at `_center` with `_radius` using the current drawing color. fn draw_circle_filled(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Draws a line from `_x1y1` to `_x2y2` using the current drawing color. fn draw_line(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Draws a single pixel at `_xy` using the current drawing color. fn draw_pixel(&mut self, _xy: PixelsXY) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Draws the outline of a rectangle from `_x1y1` to `_x2y2` using the current drawing color. fn draw_rect(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Draws a filled rectangle from `_x1y1` to `_x2y2` using the current drawing color. fn draw_rect_filled(&mut self, _x1y1: PixelsXY, _x2y2: PixelsXY) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "No graphics support in this console")) + Err(io::Error::other("No graphics support in this console")) } /// Causes any buffered output to be synced. diff --git a/std/src/console/readline.rs b/std/src/console/readline.rs index d1672ce0..1bb000a3 100644 --- a/std/src/console/readline.rs +++ b/std/src/console/readline.rs @@ -321,10 +321,7 @@ pub async fn read_line( /// suppress echo. pub async fn read_line_secure(console: &mut dyn Console, prompt: &str) -> io::Result { if !console.is_interactive() { - return Err(io::Error::new( - io::ErrorKind::Other, - "Cannot read secure strings from a raw console".to_owned(), - )); + return Err(io::Error::other("Cannot read secure strings from a raw console".to_owned())); } read_line_interactive(console, prompt, "", None, false).await } diff --git a/std/src/gpio/fakes.rs b/std/src/gpio/fakes.rs index b9298954..8ab30402 100644 --- a/std/src/gpio/fakes.rs +++ b/std/src/gpio/fakes.rs @@ -26,23 +26,23 @@ pub(crate) struct NoopPins {} impl Pins for NoopPins { fn setup(&mut self, _pin: Pin, _mode: PinMode) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "GPIO backend not compiled in")) + Err(io::Error::other("GPIO backend not compiled in")) } fn clear(&mut self, _pin: Pin) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "GPIO backend not compiled in")) + Err(io::Error::other("GPIO backend not compiled in")) } fn clear_all(&mut self) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "GPIO backend not compiled in")) + Err(io::Error::other("GPIO backend not compiled in")) } fn read(&mut self, _pin: Pin) -> io::Result { - Err(io::Error::new(io::ErrorKind::Other, "GPIO backend not compiled in")) + Err(io::Error::other("GPIO backend not compiled in")) } fn write(&mut self, _pin: Pin, _v: bool) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "GPIO backend not compiled in")) + Err(io::Error::other("GPIO backend not compiled in")) } } diff --git a/std/src/program.rs b/std/src/program.rs index b32df942..2b82a45d 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -87,7 +87,7 @@ impl Program for ImmutableProgram { } async fn edit(&mut self, _console: &mut dyn Console) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "Editing not supported")) + Err(io::Error::other("Editing not supported")) } fn load(&mut self, name: Option<&str>, text: &str) { diff --git a/std/src/storage/mod.rs b/std/src/storage/mod.rs index 9d392f55..73fe0ef3 100644 --- a/std/src/storage/mod.rs +++ b/std/src/storage/mod.rs @@ -34,7 +34,7 @@ pub use mem::*; pub(crate) fn time_format_error_to_io_error(e: Format) -> io::Error { match e { Format::StdIo(e) => e, - e => io::Error::new(io::ErrorKind::Other, format!("{}", e)), + e => io::Error::other(format!("{}", e)), } } @@ -154,7 +154,7 @@ pub trait Drive { /// Gets the ACLs of the file `_name`. async fn get_acls(&self, _name: &str) -> io::Result { - Err(io::Error::new(io::ErrorKind::Other, "Operation not supported by drive")) + Err(io::Error::other("Operation not supported by drive")) } /// Saves the in-memory program given by `content` into `name`. @@ -168,7 +168,7 @@ pub trait Drive { _add: &FileAcls, _remove: &FileAcls, ) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "Operation not supported by drive")) + Err(io::Error::other("Operation not supported by drive")) } /// Gets the system-addressable path of the file `_name`, if any. diff --git a/std/src/testutils.rs b/std/src/testutils.rs index 28fc10cf..49adfa55 100644 --- a/std/src/testutils.rs +++ b/std/src/testutils.rs @@ -265,7 +265,7 @@ impl Console for MockConsole { fn size_pixels(&self) -> io::Result { match self.size_pixels { Some(size) => Ok(size), - None => Err(io::Error::new(io::ErrorKind::Other, "Graphical console size not yet set")), + None => Err(io::Error::other("Graphical console size not yet set")), } } diff --git a/web/src/canvas.rs b/web/src/canvas.rs index 6abc2aef..12d16c30 100644 --- a/web/src/canvas.rs +++ b/web/src/canvas.rs @@ -41,9 +41,9 @@ const DEFAULT_FONT_SIZE: u16 = 16; /// Converts a `JsValue` error to an `io::Error`. pub(crate) fn js_value_to_io_error(e: JsValue) -> io::Error { if let Some(str) = e.as_string() { - return io::Error::new(io::ErrorKind::Other, str); + return io::Error::other(str); } - io::Error::new(io::ErrorKind::Other, "Unknown error") + io::Error::other("Unknown error") } /// Returns the 2D rendering context for a given `canvas` element. diff --git a/web/src/store.rs b/web/src/store.rs index 6c98b7d0..a2a50c43 100644 --- a/web/src/store.rs +++ b/web/src/store.rs @@ -151,12 +151,12 @@ impl WebDrive { fn fixup_names(&mut self) -> io::Result<()> { let n = match self.storage.length() { Ok(n) => n, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", e))), + Err(e) => return Err(io::Error::other(format!("{:?}", e))), }; for i in 0..n { let key = match self.storage.key(i) { Ok(Some(key)) => key, - Ok(None) => return Err(io::Error::new(io::ErrorKind::Other, "Entry vanished")), + Ok(None) => return Err(io::Error::other("Entry vanished")), Err(e) => { return Err(io::Error::new( io::ErrorKind::Other, @@ -258,12 +258,12 @@ impl Drive for WebDrive { let n = match self.storage.length() { Ok(n) => n, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", e))), + Err(e) => return Err(io::Error::other(format!("{:?}", e))), }; for i in 0..n { let key = match self.storage.key(i) { Ok(Some(key)) => key, - Ok(None) => return Err(io::Error::new(io::ErrorKind::Other, "Entry vanished")), + Ok(None) => return Err(io::Error::other("Entry vanished")), Err(e) => { return Err(io::Error::new( io::ErrorKind::Other, From 036b742b2065cfe275fc1633dd39479b5380fff6 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 5 Dec 2025 18:25:28 -0800 Subject: [PATCH 056/110] Only run workflows on push This should avoid duplicate workflow runs when a pull request is created or modified. We just care about branch changes. --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9147fcaa..dc144b92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ name: Build release -on: [push, pull_request] +on: push jobs: linux-armv7-rpi: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a79686d6..917ed9cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ name: Test -on: [push, pull_request] +on: push jobs: lint: From b7fc420fe277113a4ac394a112d8465e17647cbe Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 5 Dec 2025 18:28:23 -0800 Subject: [PATCH 057/110] Fix Clippy warnings with Rust 1.91 --- core/src/parser.rs | 2 +- std/src/console/cmds.rs | 2 +- std/src/console/linebuffer.rs | 2 +- std/src/help.rs | 2 +- std/src/testutils.rs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index b91131b2..c2eab61e 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -1968,7 +1968,7 @@ impl Iterator for StatementIter<'_> { } /// Extracts all statements from the input stream. -pub(crate) fn parse(input: &mut dyn io::Read) -> StatementIter { +pub(crate) fn parse(input: &mut dyn io::Read) -> StatementIter<'_> { StatementIter { parser: Parser::from(input) } } diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index d4aa272a..48fb1d55 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -576,7 +576,7 @@ impl Callable for PrintCommand { } ArgSep::Long => { text += " "; - while text.len() % 14 != 0 { + while !text.len().is_multiple_of(14) { text += " "; } } diff --git a/std/src/console/linebuffer.rs b/std/src/console/linebuffer.rs index 68f98dda..577b1f6a 100644 --- a/std/src/console/linebuffer.rs +++ b/std/src/console/linebuffer.rs @@ -44,7 +44,7 @@ impl LineBuffer { } /// Gets and iterator over buffer chars. - pub fn chars(&self) -> Chars { + pub fn chars(&self) -> Chars<'_> { self.line.chars() } diff --git a/std/src/help.rs b/std/src/help.rs index 4f9a2b14..585eb7a1 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -395,7 +395,7 @@ impl Topics { } /// Returns an iterator over all the topics. - fn values(&self) -> radix_trie::iter::Values> { + fn values(&self) -> radix_trie::iter::Values<'_, String, Box> { self.0.values() } } diff --git a/std/src/testutils.rs b/std/src/testutils.rs index 49adfa55..8fe7edf3 100644 --- a/std/src/testutils.rs +++ b/std/src/testutils.rs @@ -507,7 +507,7 @@ impl Tester { /// Runs `script` in the configured machine and returns a `Checker` object to validate /// expectations about the execution. - pub fn run>(&mut self, script: S) -> Checker { + pub fn run>(&mut self, script: S) -> Checker<'_> { let result = block_on(self.machine.exec(&mut script.into().as_bytes())); Checker::new(self, result) } @@ -520,7 +520,7 @@ impl Tester { /// /// This is useful when compared to `run` because `Machine::exec` compiles the script as one /// unit and thus compilation errors may prevent validating other operations later on. - pub fn run_n(&mut self, scripts: &[&str]) -> Checker { + pub fn run_n(&mut self, scripts: &[&str]) -> Checker<'_> { let mut result = Ok(StopReason::Eof); for script in scripts { result = block_on(self.machine.exec(&mut script.as_bytes())); From b5e576234313e4e333ae0267a7ba8ff475958537 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 23 Dec 2025 10:02:57 +0100 Subject: [PATCH 058/110] Fix Clippy warnings with Rust 1.92 --- std/src/console/graphics.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/std/src/console/graphics.rs b/std/src/console/graphics.rs index 69f7e01c..b8addaec 100644 --- a/std/src/console/graphics.rs +++ b/std/src/console/graphics.rs @@ -121,10 +121,7 @@ impl ClampedMul for u16 { impl ClampedMul for u16 { fn clamped_mul(self, rhs: u16) -> i32 { - match i32::from(self).checked_mul(i32::from(rhs)) { - Some(v) => v, - None => i32::MAX, - } + i32::from(self).checked_mul(i32::from(rhs)).unwrap_or(i32::MAX) } } From f84f29f8238512c290afe384fb4c26b1562a3eb8 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Wed, 12 Nov 2025 18:35:01 -0500 Subject: [PATCH 059/110] Switch to use getoptsargs This leverages a new crate I'v published that encapsulates options and arguments parsing and lets me remove some unnecessary code from here. --- cli/Cargo.toml | 2 +- cli/src/main.rs | 128 +++++++++------------------------ cli/tests/cli/help.out | 7 +- cli/tests/cli/help.out.rpi | 7 +- cli/tests/cli/help.out.rpi.sdl | 7 +- cli/tests/cli/help.out.sdl | 7 +- cli/tests/integration_test.rs | 6 +- 7 files changed, 59 insertions(+), 105 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6217e9b9..86d9412c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -21,7 +21,7 @@ rpi = ["endbasic-rpi", "endbasic-st7735s"] anyhow = "1.0" async-channel = "2.2" dirs = "5.0" -getopts = "0.2" +getoptsargs = "0.1" thiserror = "1.0" [dependencies.endbasic-client] diff --git a/cli/src/main.rs b/cli/src/main.rs index 2e72ea17..640ebc81 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -27,13 +27,11 @@ use async_channel::Sender; use endbasic_core::exec::Signal; use endbasic_std::console::{Console, ConsoleSpec}; use endbasic_std::storage::Storage; -use getopts::Options; +use getoptsargs::prelude::*; use std::cell::RefCell; -use std::env; use std::fs::File; use std::io; use std::path::Path; -use std::process; use std::rc::Rc; /// Errors caused by the user when invoking this binary (invalid options or arguments). @@ -50,53 +48,28 @@ impl UsageError { } } -/// Consumes and returns the program name from `env::Args`. -/// -/// If the program name cannot be obtained, return `default_name` instead. -fn program_name(mut args: env::Args, default_name: &'static str) -> (String, env::Args) { - let name = match args.next() { - Some(arg0) => match Path::new(&arg0).file_stem() { - Some(basename) => match basename.to_str() { - Some(s) => s.to_owned(), - None => default_name.to_owned(), - }, - None => default_name.to_owned(), - }, - None => default_name.to_owned(), - }; - (name, args) -} - -/// Prints usage information for program `name` with `opts` following the GNU Standards format. -fn help(name: &str, opts: &Options) { - let brief = format!("Usage: {} [options] [program-file]", name); - println!("{}", opts.usage(&brief)); - println!("CONSOLE-SPEC can be one of the following:"); +/// Prints extra usage information into `o`. +fn app_extra_help(o: &mut dyn io::Write) -> io::Result<()> { + writeln!(o, "CONSOLE-SPEC can be one of the following:")?; if cfg!(feature = "sdl") { - println!(" sdl[:SPEC] enables the graphical console and configures it"); - println!(" with the settings in SPEC, which is of the form:"); - println!(" resolution=RESOLUTION,fg_color=COLOR,bg_color=COLOR,"); - println!(" font_path=TTF,font_size=SIZE"); - println!(" individual components of the SPEC can be omitted"); - println!(" RESOLUTION can be one of 'fs' (for full screen),"); - println!(" 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'"); + writeln!(o, " sdl[:SPEC] enables the graphical console and configures it")?; + writeln!(o, " with the settings in SPEC, which is of the form:")?; + writeln!( + o, + " resolution=RESOLUTION,fg_color=COLOR,bg_color=COLOR," + )?; + writeln!(o, " font_path=TTF,font_size=SIZE")?; + writeln!(o, " individual components of the SPEC can be omitted")?; + writeln!(o, " RESOLUTION can be one of 'fs' (for full screen),")?; + writeln!(o, " 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs'")?; } if cfg!(feature = "rpi") { - println!(" st7735s[:SPEC] enables the ST7735S LCD console and configures it"); - println!(" with the settings in SPEC, which is of the form:"); - println!(" fg_color=COLOR,bg_color=COLOR,font=NAME"); + writeln!(o, " st7735s[:SPEC] enables the ST7735S LCD console and configures it")?; + writeln!(o, " with the settings in SPEC, which is of the form:")?; + writeln!(o, " fg_color=COLOR,bg_color=COLOR,font=NAME")?; } - println!(" text enables the text-based console"); - println!(); - println!("Report bugs to: https://github.com/endbasic/endbasic/issues"); - println!("EndBASIC home page: https://www.endbasic.dev/"); -} - -/// Prints version information following the GNU Standards format. -fn version() { - println!("EndBASIC {}", env!("CARGO_PKG_VERSION")); - println!("Copyright 2020-2025 Julio Merino"); - println!("License Apache Version 2.0 "); + writeln!(o, " text enables the text-based console")?; + Ok(()) } /// Creates a new EndBASIC machine builder based on the features enabled in this crate. @@ -344,36 +317,28 @@ async fn run_interactive( } } -/// Version of `main` that returns errors to the caller for reporting. -async fn safe_main(name: &str, args: env::Args) -> Result { - let args: Vec = args.collect(); - - let mut opts = Options::new(); - opts.optopt("", "console", "type and properties of the console to use", "CONSOLE-SPEC"); - opts.optflag("h", "help", "show command-line usage information and exit"); - opts.optflag("i", "interactive", "force interactive mode when running a script"); - opts.optopt("", "local-drive", "location of the drive to mount as LOCAL", "URI"); - opts.optopt("", "service-url", "base URL of the cloud service", "URL"); - opts.optflag("", "version", "show version information and exit"); - let matches = opts.parse(args)?; - - if matches.opt_present("help") { - help(name, &opts); - return Ok(0); - } - - if matches.opt_present("version") { - version(); - return Ok(0); - } +fn app_build(builder: Builder) -> Builder { + builder + .copyright("Copyright 2020-2025 Julio Merino") + .license(License::Apache2) + .homepage("https://www.endbasic.dev/") + .bugs("https://github.com/endbasic/endbasic/issues") + .optopt("", "console", "type and properties of the console to use", "CONSOLE-SPEC") + .optflag("i", "interactive", "force interactive mode when running a script") + .optopt("", "local-drive", "location of the drive to mount as LOCAL", "URI") + .optopt("", "service-url", "base URL of the cloud service", "URL") + .trailarg("program-file", 0, 1, "script to execute without entering interactive mode") + .extra_help(app_extra_help) +} +async fn app_main(matches: Matches) -> Result { let console_spec = matches.opt_str("console"); let service_url = matches .opt_str("service-url") .unwrap_or_else(|| endbasic_client::PROD_API_ADDRESS.to_owned()); - match matches.free.as_slice() { + match matches.arg_trail() { [] => { let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?; Ok(run_repl_loop(console_spec.as_deref(), &local_drive, &service_url).await?) @@ -391,27 +356,4 @@ async fn safe_main(name: &str, args: env::Args) -> Result { } } -#[tokio::main] -async fn main() { - let (name, args) = program_name(env::args(), "endbasic"); - let exit_code = match safe_main(&name, args).await { - Ok(code) => code, - Err(e) => { - if let Some(e) = e.downcast_ref::() { - eprintln!("Usage error: {}", e); - eprintln!("Type {} --help for more information", name); - 2 - } else if let Some(e) = e.downcast_ref::() { - eprintln!("Usage error: {}", e); - eprintln!("Type {} --help for more information", name); - 2 - } else { - eprintln!("{}: {}", name, e); - 1 - } - } - }; - // There should not be any interesting destructors left behind when calling this, or else they - // will not run. - process::exit(exit_code); -} +tokio_app!("EndBASIC", app_build, app_main); diff --git a/cli/tests/cli/help.out b/cli/tests/cli/help.out index e81d6b52..c3fe3c3a 100644 --- a/cli/tests/cli/help.out +++ b/cli/tests/cli/help.out @@ -1,15 +1,18 @@ Usage: endbasic [options] [program-file] Options: + -h, --help show command-line usage information and exit + --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use - -h, --help show command-line usage information and exit -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL --service-url URL base URL of the cloud service - --version show version information and exit + +Arguments: + [program-file] script to execute without entering interactive mode CONSOLE-SPEC can be one of the following: text enables the text-based console diff --git a/cli/tests/cli/help.out.rpi b/cli/tests/cli/help.out.rpi index d815db08..32083c78 100644 --- a/cli/tests/cli/help.out.rpi +++ b/cli/tests/cli/help.out.rpi @@ -1,15 +1,18 @@ Usage: endbasic [options] [program-file] Options: + -h, --help show command-line usage information and exit + --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use - -h, --help show command-line usage information and exit -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL --service-url URL base URL of the cloud service - --version show version information and exit + +Arguments: + [program-file] script to execute without entering interactive mode CONSOLE-SPEC can be one of the following: st7735s enables the ST7735S LCD console diff --git a/cli/tests/cli/help.out.rpi.sdl b/cli/tests/cli/help.out.rpi.sdl index 91f41c83..2b4c8de4 100644 --- a/cli/tests/cli/help.out.rpi.sdl +++ b/cli/tests/cli/help.out.rpi.sdl @@ -1,15 +1,18 @@ Usage: endbasic [options] [program-file] Options: + -h, --help show command-line usage information and exit + --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use - -h, --help show command-line usage information and exit -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL --service-url URL base URL of the cloud service - --version show version information and exit + +Arguments: + [program-file] script to execute without entering interactive mode CONSOLE-SPEC can be one of the following: sdl[:SPEC] enables the graphical console and configures it diff --git a/cli/tests/cli/help.out.sdl b/cli/tests/cli/help.out.sdl index 317ae523..6f1c585b 100644 --- a/cli/tests/cli/help.out.sdl +++ b/cli/tests/cli/help.out.sdl @@ -1,15 +1,18 @@ Usage: endbasic [options] [program-file] Options: + -h, --help show command-line usage information and exit + --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use - -h, --help show command-line usage information and exit -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL --service-url URL base URL of the cloud service - --version show version information and exit + +Arguments: + [program-file] script to execute without entering interactive mode CONSOLE-SPEC can be one of the following: sdl[:SPEC] enables the graphical console and configures it diff --git a/cli/tests/integration_test.rs b/cli/tests/integration_test.rs index bbbbffcb..cc1d98df 100644 --- a/cli/tests/integration_test.rs +++ b/cli/tests/integration_test.rs @@ -297,7 +297,7 @@ fn test_cli_program_name_uses_arg0() { Behavior::Null, Behavior::Null, Behavior::Literal( - "Usage error: Too many arguments\nType custom-name --help for more information\n" + "Usage error: Too many arguments\nType `custom-name --help` for more information\n" .to_owned(), ), ); @@ -312,7 +312,7 @@ fn test_cli_too_many_args() { Behavior::Null, Behavior::Null, Behavior::Literal( - "Usage error: Too many arguments\nType endbasic --help for more information\n" + "Usage error: Too many arguments\nType `endbasic --help` for more information\n" .to_owned(), ), ); @@ -327,7 +327,7 @@ fn test_cli_unknown_option() { Behavior::Null, Behavior::Null, Behavior::Literal( - "Usage error: Unrecognized option: 'Z'\nType endbasic --help for more information\n" + "Usage error: Unrecognized option: 'Z'\nType `endbasic --help` for more information\n" .to_owned(), ), ); From f7dca3d0fdbf4c460f5f53d49c519b0f3898c299 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 1 Jan 2026 10:15:47 +0100 Subject: [PATCH 060/110] Add 2026 to general copyright notices --- NOTICE | 2 +- cli/NOTICE | 2 +- cli/src/main.rs | 2 +- client/NOTICE | 2 +- core/NOTICE | 2 +- repl/NOTICE | 2 +- repl/src/lib.rs | 2 +- rpi/NOTICE | 2 +- sdl/NOTICE | 2 +- st7735s/NOTICE | 2 +- std/NOTICE | 2 +- terminal/NOTICE | 2 +- web/NOTICE | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/NOTICE b/NOTICE index f03acae8..a6ebc462 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/cli/NOTICE b/cli/NOTICE index f03acae8..a6ebc462 100644 --- a/cli/NOTICE +++ b/cli/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/cli/src/main.rs b/cli/src/main.rs index 640ebc81..3f16707c 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -319,7 +319,7 @@ async fn run_interactive( fn app_build(builder: Builder) -> Builder { builder - .copyright("Copyright 2020-2025 Julio Merino") + .copyright("Copyright 2020-2026 Julio Merino") .license(License::Apache2) .homepage("https://www.endbasic.dev/") .bugs("https://github.com/endbasic/endbasic/issues") diff --git a/client/NOTICE b/client/NOTICE index f03acae8..a6ebc462 100644 --- a/client/NOTICE +++ b/client/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/core/NOTICE b/core/NOTICE index f03acae8..a6ebc462 100644 --- a/core/NOTICE +++ b/core/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/repl/NOTICE b/repl/NOTICE index f03acae8..a6ebc462 100644 --- a/repl/NOTICE +++ b/repl/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/repl/src/lib.rs b/repl/src/lib.rs index bd424052..fb8632bc 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -42,7 +42,7 @@ pub fn print_welcome(console: Rc>) -> io::Result<()> { } else { console.print("")?; console.print(&format!(" EndBASIC {}", env!("CARGO_PKG_VERSION")))?; - console.print(" Copyright 2020-2025 Julio Merino")?; + console.print(" Copyright 2020-2026 Julio Merino")?; console.print("")?; console.print(" Type HELP for interactive usage information.")?; } diff --git a/rpi/NOTICE b/rpi/NOTICE index 2fd5c019..2a93e38c 100644 --- a/rpi/NOTICE +++ b/rpi/NOTICE @@ -1,5 +1,5 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino ---- diff --git a/sdl/NOTICE b/sdl/NOTICE index f03acae8..a6ebc462 100644 --- a/sdl/NOTICE +++ b/sdl/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/st7735s/NOTICE b/st7735s/NOTICE index 2fd5c019..2a93e38c 100644 --- a/st7735s/NOTICE +++ b/st7735s/NOTICE @@ -1,5 +1,5 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino ---- diff --git a/std/NOTICE b/std/NOTICE index f03acae8..a6ebc462 100644 --- a/std/NOTICE +++ b/std/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/terminal/NOTICE b/terminal/NOTICE index f03acae8..a6ebc462 100644 --- a/terminal/NOTICE +++ b/terminal/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino diff --git a/web/NOTICE b/web/NOTICE index f03acae8..a6ebc462 100644 --- a/web/NOTICE +++ b/web/NOTICE @@ -1,2 +1,2 @@ EndBASIC -Copyright 2020-2025 Julio Merino +Copyright 2020-2026 Julio Merino From 01c1f3b3e808e75f990441ae8cd5783741920da7 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 1 Jan 2026 23:23:13 +0100 Subject: [PATCH 061/110] Add support for EXIT FOR --- NEWS.md | 2 + cli/tests/repl/help.out | 2 + core/README.md | 3 +- core/src/ast.rs | 9 ++- core/src/compiler/mod.rs | 152 +++++++++++++++++++++++++++++++++++---- core/src/exec.rs | 33 +++++++++ core/src/parser.rs | 35 +++++---- std/src/lang.md | 2 + 8 files changed, 209 insertions(+), 29 deletions(-) diff --git a/NEWS.md b/NEWS.md index f38d07c9..b0c4d8a4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -52,6 +52,8 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Fixed the rendering of zero-sized rectangles in the SDL console so that debug builds don't crash and so that the behavior matches the web console. + +* Added support for `EXIT FOR`. ## Changes in version 0.11.1 diff --git a/cli/tests/repl/help.out b/cli/tests/repl/help.out index 889d9728..097dd1ef 100644 --- a/cli/tests/repl/help.out +++ b/cli/tests/repl/help.out @@ -375,6 +375,8 @@ Output from HELP "FOR": PRINT a NEXT + `FOR` loops can be exited an any point via the `EXIT FOR` statement. + Output from HELP "FUNCTIONS":  User-defined functions diff --git a/core/README.md b/core/README.md index baffc5eb..efe3068f 100644 --- a/core/README.md +++ b/core/README.md @@ -45,7 +45,8 @@ currently supports: * `IF ... THEN ... [ELSE ...]` uniline statements. * `IF ... THEN` / `ELSEIF ... THEN` / `ELSE` / `END IF` multiline statements. -* `FOR x = ... TO ... [STEP ...]` / `NEXT` loops. +* `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 ...` / diff --git a/core/src/ast.rs b/core/src/ast.rs index 1d6d5e08..3c5c7fa8 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -607,9 +607,9 @@ pub struct EndSpan { pub code: Option, } -/// Components of an `EXIT DO` statement. +/// Components of an `EXIT` statement. #[derive(Debug, Eq, PartialEq)] -pub struct ExitDoSpan { +pub struct ExitSpan { /// Position of the statement. pub pos: LineCol, } @@ -807,7 +807,10 @@ pub enum Statement { End(EndSpan), /// Represents an `EXIT DO` statement. - ExitDo(ExitDoSpan), + ExitDo(ExitSpan), + + /// Represents an `EXIT FOR` statement. + ExitFor(ExitSpan), /// Represents a `FOR` statement. For(ForSpan), diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 765d7958..4c13bba8 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -59,8 +59,8 @@ pub enum Error { #[error("{0}: I/O error during compilation: {1}")] IoError(LineCol, io::Error), - #[error("{0}: EXIT DO outside of DO loop")] - MisplacedExitDo(LineCol), + #[error("{0}: EXIT {1} outside of {1} loop")] + MisplacedExit(LineCol, &'static str), #[error("{0}: {1} requires a boolean condition")] NotABooleanCondition(LineCol, String), @@ -257,8 +257,8 @@ struct Fixup { } impl Fixup { - /// Constructs a `Fixup` for an `EXIT DO` instruction. - fn from_exit_do(target: String, span: ExitDoSpan) -> Self { + /// Constructs a `Fixup` for an `EXIT` instruction. + fn from_exit(target: String, span: ExitSpan) -> Self { Self { target, target_pos: span.pos, ftype: FixupType::Goto } } @@ -291,6 +291,13 @@ struct Compiler { /// the first component has to be incremented. exit_do_level: (usize, usize), + /// Current nesting of `FOR` loops, needed to assign targets for `EXIT FOR` statements. + /// + /// The first component of this pair indicates which block of `EXIT FOR`s we are dealing with and + /// the second component indicates their nesting. Every time the second component reaches zero, + /// the first component has to be incremented. + exit_for_level: (usize, usize), + /// Current number of `SELECT` statements, needed to assign internal variable names. selects: usize, @@ -325,14 +332,15 @@ impl Compiler { pc } - /// Generates a fake label for the end of a `DO` loop based on the current nesting `level`. + /// Generates a fake label for the end of a scope based on the current nesting `level` and the + /// `name` of the loop. /// - /// This is a little hack to reuse the same machinery that handles `GOTO`s to handle early exits - /// in `DO` loops. We can do this because we know that users cannot specify custom labels that + /// This is a little hack to reuse the same machinery that handles `GOTO`s to handle early + /// `EXIT`s. We can do this because we know that users cannot specify custom labels that /// start with a digit and all user-provided labels that do start with a digit are also fully /// numeric. - fn do_label(level: (usize, usize)) -> String { - format!("0do{}_{}", level.0, level.1) + fn exit_label(name: &'static str, level: (usize, usize)) -> String { + format!("0{}{}_{}", name, level.0, level.1) } /// Generates the name of the symbol that holds the return value of the function `name`. @@ -561,7 +569,8 @@ impl Compiler { } } - let existing = self.labels.insert(Compiler::do_label(self.exit_do_level), end_pc + 1); + let existing = + self.labels.insert(Compiler::exit_label("do", self.exit_do_level), end_pc + 1); assert!(existing.is_none(), "Auto-generated label must be unique"); self.exit_do_level.1 -= 1; if self.exit_do_level.1 == 0 { @@ -579,6 +588,8 @@ impl Compiler { || 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()); let skip_pc = self.emit(Instruction::Nop); @@ -613,10 +624,18 @@ impl Compiler { self.compile_assignment(span.iter.clone(), span.iter_pos, span.next)?; - self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); + let end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); + let existing = + self.labels.insert(Compiler::exit_label("for", self.exit_for_level), end_pc + 1); + assert!(existing.is_none(), "Auto-generated label must be unique"); + self.exit_for_level.1 -= 1; + if self.exit_for_level.1 == 0 { + self.exit_for_level.0 += 1; + } + Ok(()) } @@ -912,12 +931,23 @@ impl Compiler { Statement::ExitDo(span) => { if self.exit_do_level.1 == 0 { - return Err(Error::MisplacedExitDo(span.pos)); + return Err(Error::MisplacedExit(span.pos, "DO")); } let exit_do_pc = self.emit(Instruction::Nop); self.fixups.insert( exit_do_pc, - Fixup::from_exit_do(Compiler::do_label(self.exit_do_level), span), + Fixup::from_exit(Compiler::exit_label("do", self.exit_do_level), span), + ); + } + + Statement::ExitFor(span) => { + if self.exit_for_level.1 == 0 { + return Err(Error::MisplacedExit(span.pos, "FOR")); + } + let exit_for_pc = self.emit(Instruction::Nop); + self.fixups.insert( + exit_for_pc, + Fixup::from_exit(Compiler::exit_label("for", self.exit_for_level), span), ); } @@ -1932,6 +1962,102 @@ mod tests { .check(); } + #[test] + fn test_compile_exit_for_infinite_simple() { + Tester::default() + .parse("FOR i = 1 to 10\nEXIT FOR\nNEXT") + .compile() + .expect_instr(0, Instruction::PushInteger(1, lc(1, 9))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr(3, Instruction::PushInteger(10, lc(1, 14))) + .expect_instr(4, Instruction::LessEqualIntegers(lc(1, 11))) + .expect_instr(5, Instruction::JumpIfNotTrue(12)) + .expect_instr(6, Instruction::Jump(JumpISpan { addr: 12 })) + .expect_instr(7, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr(8, Instruction::PushInteger(1, lc(1, 16))) + .expect_instr(9, Instruction::AddIntegers(lc(1, 11))) + .expect_instr(10, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(11, Instruction::Jump(JumpISpan { addr: 2 })) + .check(); + } + + #[test] + fn test_compile_exit_for_nested() { + Tester::default() + .parse("FOR i = 1 to 10\nFOR j = 2 to 20\nEXIT FOR\nNEXT\nEXIT FOR\nNEXT") + .compile() + .expect_instr(0, Instruction::PushInteger(1, lc(1, 9))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr(3, Instruction::PushInteger(10, lc(1, 14))) + .expect_instr(4, Instruction::LessEqualIntegers(lc(1, 11))) + .expect_instr(5, Instruction::JumpIfNotTrue(24)) + // Begin nested for loop. + .expect_instr(6, Instruction::PushInteger(2, lc(2, 9))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("j"))) + .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("j"), lc(2, 5))) + .expect_instr(9, Instruction::PushInteger(20, lc(2, 14))) + .expect_instr(10, Instruction::LessEqualIntegers(lc(2, 11))) + .expect_instr(11, Instruction::JumpIfNotTrue(18)) + .expect_instr(12, Instruction::Jump(JumpISpan { addr: 18 })) // Exit for. + .expect_instr(13, Instruction::LoadInteger(SymbolKey::from("j"), lc(2, 5))) + .expect_instr(14, Instruction::PushInteger(1, lc(2, 16))) + .expect_instr(15, Instruction::AddIntegers(lc(2, 11))) + .expect_instr(16, Instruction::Assign(SymbolKey::from("j"))) + .expect_instr(17, Instruction::Jump(JumpISpan { addr: 8 })) + // Begin nested for loop. + .expect_instr(18, Instruction::Jump(JumpISpan { addr: 24 })) // Exit for. + .expect_instr(19, Instruction::LoadInteger(SymbolKey::from("i"), lc(1, 5))) + .expect_instr(20, Instruction::PushInteger(1, lc(1, 16))) + .expect_instr(21, Instruction::AddIntegers(lc(1, 11))) + .expect_instr(22, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(23, Instruction::Jump(JumpISpan { addr: 2 })) + .check(); + } + + #[test] + fn test_compile_exit_for_outside_of_loop() { + Tester::default() + .parse("EXIT FOR") + .compile() + .expect_err("1:1: EXIT FOR outside of FOR loop") + .check(); + + Tester::default() + .parse("WHILE TRUE: EXIT FOR: WEND") + .compile() + .expect_err("1:13: EXIT FOR outside of FOR loop") + .check(); + } + + #[test] + fn test_compile_exit_do_and_exit_for() { + Tester::default() + .parse("DO\nFOR i = 1 to 10\nDO\nEXIT DO\nLOOP\nEXIT FOR\nNEXT\nLOOP") + .compile() + // Begin nested for loop. + .expect_instr(0, Instruction::PushInteger(1, lc(2, 9))) + .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(2, Instruction::LoadInteger(SymbolKey::from("i"), lc(2, 5))) + .expect_instr(3, Instruction::PushInteger(10, lc(2, 14))) + .expect_instr(4, Instruction::LessEqualIntegers(lc(2, 11))) + .expect_instr(5, Instruction::JumpIfNotTrue(14)) + .expect_instr(6, Instruction::Jump(JumpISpan { addr: 8 })) // Exit do. + // Begin nested do loop. + .expect_instr(7, Instruction::Jump(JumpISpan { addr: 6 })) + // End nested do loop. + .expect_instr(8, Instruction::Jump(JumpISpan { addr: 14 })) // Exit for. + .expect_instr(9, Instruction::LoadInteger(SymbolKey::from("i"), lc(2, 5))) + .expect_instr(10, Instruction::PushInteger(1, lc(2, 16))) + .expect_instr(11, Instruction::AddIntegers(lc(2, 11))) + .expect_instr(12, Instruction::Assign(SymbolKey::from("i"))) + .expect_instr(13, Instruction::Jump(JumpISpan { addr: 2 })) + // End nested for loop. + .expect_instr(14, Instruction::Jump(JumpISpan { addr: 0 })) + .check(); + } + #[test] fn test_compile_for_simple_literals() { Tester::default() diff --git a/core/src/exec.rs b/core/src/exec.rs index d3072780..6b94539a 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -2451,6 +2451,39 @@ mod tests { ); } + #[test] + fn test_exit_for() { + do_ok_test( + r#" + FOR i = 1 to 10 + IF i = 5 THEN EXIT FOR + OUT i + NEXT + "#, + &[], + &["1", "2", "3", "4"], + ); + } + + #[test] + fn test_exit_do_and_exit_for() { + do_ok_test( + r#" + FOR i = 1 to 10 + j = 5 + DO WHILE j > 0 + IF j = 2 THEN EXIT DO + IF i = 4 THEN EXIT FOR + OUT i; j + j = j - 1 + LOOP + NEXT + "#, + &[], + &["1 5", "1 4", "1 3", "2 5", "2 4", "2 3", "3 5", "3 4", "3 3"], + ); + } + #[test] fn test_expr_load_not_assigned() { do_error_test( diff --git a/core/src/parser.rs b/core/src/parser.rs index c2eab61e..e656a87c 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -669,10 +669,16 @@ impl<'a> Parser<'a> { } } - /// Parses an `EXIT DO` statement. - fn parse_exit_do(&mut self, pos: LineCol) -> Result { - self.expect_and_consume(Token::Do, "Expecting DO after EXIT")?; - Ok(Statement::ExitDo(ExitDoSpan { pos })) + /// 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 }), + _ => return Err(Error::Bad(peeked.pos, "Expecting DO or FOR after EXIT".to_owned())), + }; + self.lexer.consume_peeked(); + Ok(stmt) } /// Parses a variable list of comma-separated expressions. The caller must have consumed the @@ -1785,7 +1791,7 @@ impl<'a> Parser<'a> { 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_do(token_span.pos)?)), + 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()?)), @@ -1832,7 +1838,7 @@ impl<'a> Parser<'a> { 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_do(token_span.pos)?)), + Token::Exit => Ok(Some(self.parse_exit(token_span.pos)?)), Token::If => { let result = self.parse_if(token_span.pos); if result.is_err() { @@ -2749,13 +2755,18 @@ mod tests { #[test] fn test_exit_do() { - do_ok_test(" EXIT DO", &[Statement::ExitDo(ExitDoSpan { pos: lc(1, 3) })]); + 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_do_errors() { - do_error_test("EXIT", "1:5: Expecting DO after EXIT"); - do_error_test("EXIT 5", "1:6: Expecting DO after EXIT"); + fn test_exit_errors() { + do_error_test("EXIT", "1:5: Expecting DO or FOR after EXIT"); + do_error_test("EXIT 5", "1:6: Expecting DO or FOR after EXIT"); } /// Wrapper around `do_ok_test` to parse an expression. Given that expressions alone are not @@ -3833,9 +3844,9 @@ mod tests { #[test] fn test_if_uniline_allowed_exit() { - do_if_uniline_allowed_test("EXIT DO", Statement::ExitDo(ExitDoSpan { pos: lc(1, 11) })); + 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 after EXIT"); + do_error_test("IF 1 THEN EXIT", "1:15: Expecting DO or FOR after EXIT"); } #[test] diff --git a/std/src/lang.md b/std/src/lang.md index c06dd997..6d39ef3a 100644 --- a/std/src/lang.md +++ b/std/src/lang.md @@ -222,6 +222,8 @@ You can optionally specify a positive or negative `STEP` to change how the itera PRINT a NEXT +`FOR` loops can be exited an any point via the `EXIT FOR` statement. + # Jumps GOTO, GOSUB, END, and labels From 9aabbc91aa88d250ea8214a161f7c20d190cb555 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 6 Jan 2026 12:13:47 +0100 Subject: [PATCH 062/110] Remove "loop" from MisplacedExit errors This is to make room for misplaced EXITs for callables. --- core/src/compiler/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 4c13bba8..36fbab0c 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -59,7 +59,7 @@ pub enum Error { #[error("{0}: I/O error during compilation: {1}")] IoError(LineCol, io::Error), - #[error("{0}: EXIT {1} outside of {1} loop")] + #[error("{0}: EXIT {1} outside of {1}")] MisplacedExit(LineCol, &'static str), #[error("{0}: {1} requires a boolean condition")] @@ -1952,13 +1952,13 @@ mod tests { Tester::default() .parse("EXIT DO") .compile() - .expect_err("1:1: EXIT DO outside of DO loop") + .expect_err("1:1: EXIT DO outside of DO") .check(); Tester::default() .parse("WHILE TRUE: EXIT DO: WEND") .compile() - .expect_err("1:13: EXIT DO outside of DO loop") + .expect_err("1:13: EXIT DO outside of DO") .check(); } @@ -2021,13 +2021,13 @@ mod tests { Tester::default() .parse("EXIT FOR") .compile() - .expect_err("1:1: EXIT FOR outside of FOR loop") + .expect_err("1:1: EXIT FOR outside of FOR") .check(); Tester::default() .parse("WHILE TRUE: EXIT FOR: WEND") .compile() - .expect_err("1:13: EXIT FOR outside of FOR loop") + .expect_err("1:13: EXIT FOR outside of FOR") .check(); } From 9287334a32277eb5829878c029479398936e1a7e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 6 Jan 2026 11:17:59 +0100 Subject: [PATCH 063/110] Track if we are in a sub/function during compilation Extend the "current_function" concept to "current_callable" and track whether we are in a sub or a function during compilation. This will be needed to implement early exits from callables. --- core/src/compiler/mod.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 36fbab0c..8431e66f 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -317,7 +317,10 @@ struct Compiler { symtable: SymbolsTable, /// Name of the function being compiled, needed to set the return value in assignment operators. - current_function: Option, + /// + /// The boolean indicates whether this callable has a return value or not and is used to + /// differentiate between functions and subroutines. + current_callable: Option<(SymbolKey, bool)>, /// Callables to be compiled. callable_spans: Vec, @@ -432,9 +435,9 @@ impl Compiler { let mut key = SymbolKey::from(&vref.name()); let etype = self.compile_expr(expr, false)?; - if let Some(current_function) = self.current_function.as_ref() { - if &key == current_function { - key = Compiler::return_key(current_function); + if let Some(current_callable) = self.current_callable.as_ref() { + if key == current_callable.0 { + key = Compiler::return_key(¤t_callable.0); } } @@ -1038,9 +1041,10 @@ impl Compiler { self.symtable.insert(key, SymbolPrototype::Variable(ptype)); } - self.current_function = Some(key.clone()); + debug_assert!(self.current_callable.is_none(), "Callables cannot be nested"); + self.current_callable = Some((key.clone(), true)); self.compile_many(span.body)?; - self.current_function = None; + self.current_callable = None; let load_inst = match return_type { ExprType::Boolean => Instruction::LoadBoolean, @@ -1067,7 +1071,10 @@ impl Compiler { self.symtable.insert(key, SymbolPrototype::Variable(ptype)); } + debug_assert!(self.current_callable.is_none(), "Callables cannot be nested"); + self.current_callable = Some((key.clone(), false)); self.compile_many(span.body)?; + self.current_callable = None; self.emit(Instruction::LeaveScope); self.symtable.leave_scope(); From fffac0a768fdcb669c4586423f9a0a3d81b26dcb Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 3 Jan 2026 09:28:24 +0100 Subject: [PATCH 064/110] Add support for EXIT FUNCTION/SUB --- NEWS.md | 2 +- cli/tests/repl/help.out | 6 ++ core/README.md | 4 +- core/src/ast.rs | 6 ++ core/src/compiler/mod.rs | 163 +++++++++++++++++++++++++++++++++++++-- core/src/exec.rs | 30 +++++++ core/src/parser.rs | 26 +++++-- std/src/lang.md | 4 + 8 files changed, 225 insertions(+), 16 deletions(-) diff --git a/NEWS.md b/NEWS.md index b0c4d8a4..7e176c16 100644 --- a/NEWS.md +++ b/NEWS.md @@ -53,7 +53,7 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Fixed the rendering of zero-sized rectangles in the SDL console so that debug builds don't crash and so that the behavior matches the web console. -* Added support for `EXIT FOR`. +* Added support for `EXIT FUNCTION`, `EXIT FOR`, and `EXIT SUB`. ## Changes in version 0.11.1 diff --git a/cli/tests/repl/help.out b/cli/tests/repl/help.out index 097dd1ef..9a7248ad 100644 --- a/cli/tests/repl/help.out +++ b/cli/tests/repl/help.out @@ -397,6 +397,9 @@ Output from HELP "FUNCTIONS": Global variables can be defined via the `DIM SHARED` keyword. See the "Variables" help topic for details. + Functions can be returned from at any point via the `EXIT FUNCTION` + statement. + Output from HELP "IF":  Multiline and uniline IF statements @@ -522,6 +525,9 @@ Output from HELP "SUBROUTINES": Global variables can be defined via the `DIM SHARED` keyword. See the "Variables" help topic for details. + Subroutines can be returned from at any point via the `EXIT SUB` + statement. + Output from HELP "STYLE":  Spacing, comments, and general style diff --git a/core/README.md b/core/README.md index efe3068f..c0c68c99 100644 --- a/core/README.md +++ b/core/README.md @@ -41,7 +41,7 @@ currently supports: * `DO` / `LOOP` statements with optional `UNTIL` / `WHILE` pre- and post-guards and optional `EXIT DO` early terminations. * `DIM SHARED` for global variables. -* `FUNCTION name` / `END FUNCTION`. +* `FUNCTION name` / `EXIT FUNCTION` / `END FUNCTION`. * `IF ... THEN ... [ELSE ...]` uniline statements. * `IF ... THEN` / `ELSEIF ... THEN` / `ELSE` / `END IF` multiline statements. @@ -51,7 +51,7 @@ currently supports: * `GOTO line` / `GOTO @label` statements and `@label` annotations. * `SELECT CASE` / `CASE ...` / `CASE IS ...` / `CASE ... TO ...` / `END SELECT` statements. -* `SUB name` / `END SUB`. +* `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). diff --git a/core/src/ast.rs b/core/src/ast.rs index 3c5c7fa8..26b8419b 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -812,6 +812,12 @@ pub enum Statement { /// 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), diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 8431e66f..780ee00b 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -335,6 +335,16 @@ impl Compiler { pc } + /// Generates a fake label for the epilogue a callable based on its `name`. + /// + /// This is a little hack to reuse the same machinery that handles `GOTO`s to handle early + /// `EXIT`s. We can do this because we know that users cannot specify custom labels that + /// start with a digit and all user-provided labels that do start with a digit are also fully + /// numeric. + fn exit_label_for_callable(name: &SymbolKey) -> String { + format!("0{}", name) + } + /// Generates a fake label for the end of a scope based on the current nesting `level` and the /// `name` of the loop. /// @@ -342,7 +352,7 @@ impl Compiler { /// `EXIT`s. We can do this because we know that users cannot specify custom labels that /// start with a digit and all user-provided labels that do start with a digit are also fully /// numeric. - fn exit_label(name: &'static str, level: (usize, usize)) -> String { + fn exit_label_for_loop(name: &'static str, level: (usize, usize)) -> String { format!("0{}{}_{}", name, level.0, level.1) } @@ -573,7 +583,7 @@ impl Compiler { } let existing = - self.labels.insert(Compiler::exit_label("do", self.exit_do_level), end_pc + 1); + self.labels.insert(Compiler::exit_label_for_loop("do", self.exit_do_level), end_pc + 1); assert!(existing.is_none(), "Auto-generated label must be unique"); self.exit_do_level.1 -= 1; if self.exit_do_level.1 == 0 { @@ -631,8 +641,9 @@ impl Compiler { self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); - let existing = - self.labels.insert(Compiler::exit_label("for", self.exit_for_level), end_pc + 1); + let existing = self + .labels + .insert(Compiler::exit_label_for_loop("for", self.exit_for_level), end_pc + 1); assert!(existing.is_none(), "Auto-generated label must be unique"); self.exit_for_level.1 -= 1; if self.exit_for_level.1 == 0 { @@ -939,7 +950,7 @@ impl Compiler { let exit_do_pc = self.emit(Instruction::Nop); self.fixups.insert( exit_do_pc, - Fixup::from_exit(Compiler::exit_label("do", self.exit_do_level), span), + Fixup::from_exit(Compiler::exit_label_for_loop("do", self.exit_do_level), span), ); } @@ -950,10 +961,39 @@ impl Compiler { let exit_for_pc = self.emit(Instruction::Nop); self.fixups.insert( exit_for_pc, - Fixup::from_exit(Compiler::exit_label("for", self.exit_for_level), span), + Fixup::from_exit( + Compiler::exit_label_for_loop("for", self.exit_for_level), + span, + ), ); } + Statement::ExitFunction(span) => { + let Some(current_callable) = self.current_callable.as_ref() else { + return Err(Error::MisplacedExit(span.pos, "FUNCTION")); + }; + if !current_callable.1 { + return Err(Error::MisplacedExit(span.pos, "FUNCTION")); + } + + let exit_label = Compiler::exit_label_for_callable(¤t_callable.0); + let exit_function_pc = self.emit(Instruction::Nop); + self.fixups.insert(exit_function_pc, Fixup::from_exit(exit_label, span)); + } + + Statement::ExitSub(span) => { + let Some(current_callable) = self.current_callable.as_ref() else { + return Err(Error::MisplacedExit(span.pos, "SUB")); + }; + if current_callable.1 { + return Err(Error::MisplacedExit(span.pos, "SUB")); + } + + let exit_label = Compiler::exit_label_for_callable(¤t_callable.0); + let exit_sub_pc = self.emit(Instruction::Nop); + self.fixups.insert(exit_sub_pc, Fixup::from_exit(exit_label, span)); + } + Statement::For(span) => { self.compile_for(span)?; } @@ -1052,12 +1092,16 @@ impl Compiler { ExprType::Integer => Instruction::LoadInteger, ExprType::Text => Instruction::LoadString, }; - self.emit(load_inst(return_value.clone(), span.end_pos)); + let epilogue_pc = self.emit(load_inst(return_value.clone(), span.end_pos)); self.emit(Instruction::LeaveScope); self.symtable.leave_scope(); self.emit(Instruction::Return(span.end_pos)); + let existing = + self.labels.insert(Compiler::exit_label_for_callable(&key), epilogue_pc); + assert!(existing.is_none(), "Auto-generated label must be unique"); + functions.insert(key, pc); } None => { @@ -1076,10 +1120,14 @@ impl Compiler { self.compile_many(span.body)?; self.current_callable = None; - self.emit(Instruction::LeaveScope); + let epilogue_pc = self.emit(Instruction::LeaveScope); self.symtable.leave_scope(); self.emit(Instruction::Return(span.end_pos)); + let existing = + self.labels.insert(Compiler::exit_label_for_callable(&key), epilogue_pc); + assert!(existing.is_none(), "Auto-generated label must be unique"); + subs.insert(key, pc); } } @@ -2240,6 +2288,60 @@ mod tests { .check(); } + #[test] + fn test_compile_function_early_exit() { + Tester::default() + .parse("FUNCTION foo: a = 3: EXIT FUNCTION: a = 4: END FUNCTION") + .compile() + .expect_instr(0, Instruction::Jump(JumpISpan { addr: 11 })) + .expect_instr(1, Instruction::EnterScope) + .expect_instr( + 2, + Instruction::Dim(DimISpan { + name: SymbolKey::from("0return_foo"), + shared: false, + vtype: ExprType::Integer, + }), + ) + .expect_instr(3, Instruction::PushInteger(3, lc(1, 19))) + .expect_instr(4, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(5, Instruction::Jump(JumpISpan { addr: 8 })) + .expect_instr(6, Instruction::PushInteger(4, lc(1, 41))) + .expect_instr(7, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("0return_foo"), lc(1, 44))) + .expect_instr(9, Instruction::LeaveScope) + .expect_instr(10, Instruction::Return(lc(1, 44))) + .expect_symtable( + SymbolKey::from("foo"), + SymbolPrototype::Callable( + CallableMetadataBuilder::new("USER DEFINED FUNCTION") + .with_syntax(&[(&[], None)]) + .with_category("User defined") + .with_description("User defined function") + .build(), + ), + ) + .check(); + } + + #[test] + fn test_compile_function_misplaced_exit() { + Tester::default() + .parse("FUNCTION a: END FUNCTION: EXIT FUNCTION") + .compile() + .expect_err("1:27: EXIT FUNCTION outside of FUNCTION") + .check(); + } + + #[test] + fn test_compile_function_mismatched_exit() { + Tester::default() + .parse("FUNCTION a: EXIT SUB: END FUNCTION") + .compile() + .expect_err("1:13: EXIT SUB outside of SUB") + .check(); + } + #[test] fn test_compile_function_redefined_was_variable() { Tester::default() @@ -2267,6 +2369,51 @@ mod tests { .check(); } + #[test] + fn test_compile_sub_early_exit() { + Tester::default() + .parse("SUB foo: a = 3: EXIT SUB: a = 4: END SUB") + .compile() + .expect_instr(0, Instruction::Jump(JumpISpan { addr: 9 })) + .expect_instr(1, Instruction::EnterScope) + .expect_instr(2, Instruction::PushInteger(3, lc(1, 14))) + .expect_instr(3, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(4, Instruction::Jump(JumpISpan { addr: 7 })) + .expect_instr(5, Instruction::PushInteger(4, lc(1, 31))) + .expect_instr(6, Instruction::Assign(SymbolKey::from("a"))) + .expect_instr(7, Instruction::LeaveScope) + .expect_instr(8, Instruction::Return(lc(1, 34))) + .expect_symtable( + SymbolKey::from("foo"), + SymbolPrototype::Callable( + CallableMetadataBuilder::new("USER DEFINED SUB") + .with_syntax(&[(&[], None)]) + .with_category("User defined") + .with_description("User defined sub") + .build(), + ), + ) + .check(); + } + + #[test] + fn test_compile_sub_misplaced_exit() { + Tester::default() + .parse("SUB a: END SUB: EXIT SUB") + .compile() + .expect_err("1:17: EXIT SUB outside of SUB") + .check(); + } + + #[test] + fn test_compile_sub_mismatched_exit() { + Tester::default() + .parse("SUB a: EXIT FUNCTION: END SUB") + .compile() + .expect_err("1:8: EXIT FUNCTION outside of FUNCTION") + .check(); + } + #[test] fn test_compile_sub_redefined_was_variable() { Tester::default() diff --git a/core/src/exec.rs b/core/src/exec.rs index 6b94539a..02e07afc 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -3489,6 +3489,21 @@ mod tests { do_error_test(code, &[], &[], "3:13: F is not a command"); } + #[test] + fn test_user_functions_early_exit() { + let code = r#" + FUNCTION maybe_exit(i%) + maybe_exit = 1 + IF i > 2 THEN EXIT FUNCTION + maybe_exit = 2 + END FUNCTION + FOR i = 0 to 5 + OUT maybe_exit(i) + NEXT + "#; + do_ok_test(code, &[], &["2", "2", "2", "1", "1", "1"]); + } + #[test] fn test_user_subs_recursion() { let code = r#" @@ -3538,4 +3553,19 @@ mod tests { "#; do_error_test(code, &[], &[], "5:13: FOO expected n%"); } + + #[test] + fn test_user_subs_early_exit() { + let code = r#" + SUB maybe_exit(i%) + OUT 1 + IF i > 2 THEN EXIT SUB + OUT 2 + END SUB + FOR i = 0 to 5 + maybe_exit(i) + NEXT + "#; + do_ok_test(code, &[], &["1", "2", "1", "2", "1", "2", "1", "1", "1"]); + } } diff --git a/core/src/parser.rs b/core/src/parser.rs index e656a87c..96129ed6 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -675,7 +675,14 @@ impl<'a> Parser<'a> { let stmt = match peeked.token { Token::Do => Statement::ExitDo(ExitSpan { pos }), Token::For => Statement::ExitFor(ExitSpan { pos }), - _ => return Err(Error::Bad(peeked.pos, "Expecting DO or FOR after EXIT".to_owned())), + 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) @@ -1393,7 +1400,6 @@ impl<'a> Parser<'a> { } } - // TODO(jmmv): Handle `EXIT FUNCTION` or `EXIT SUB`. _ => match self.parse_one_safe()? { Some(stmt) => body.push(stmt), None => { @@ -2763,10 +2769,20 @@ mod tests { 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 or FOR after EXIT"); - do_error_test("EXIT 5", "1:6: Expecting DO or FOR after EXIT"); + 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 @@ -3846,7 +3862,7 @@ mod tests { 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 or FOR after EXIT"); + do_error_test("IF 1 THEN EXIT", "1:15: Expecting DO, FOR, FUNCTION or SUB after EXIT"); } #[test] diff --git a/std/src/lang.md b/std/src/lang.md index 6d39ef3a..84d6337b 100644 --- a/std/src/lang.md +++ b/std/src/lang.md @@ -287,6 +287,8 @@ To define a function, use the `FUNCTION` keyword followed by an arbitrary list o Global variables can be defined via the `DIM SHARED` keyword. See the "Variables" help topic for details. +Functions can be returned from at any point via the `EXIT FUNCTION` statement. + # Subroutines User-defined subroutines @@ -301,3 +303,5 @@ To define a subroutine (also known as procedure or command), use the `SUB` keywo my_command FALSE, 8 ' Prints 1.1. Global variables can be defined via the `DIM SHARED` keyword. See the "Variables" help topic for details. + +Subroutines can be returned from at any point via the `EXIT SUB` statement. From 621e3761333670eeca0114c35b330d29f8723d26 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 6 Jan 2026 12:20:51 +0100 Subject: [PATCH 065/110] Upgrade to sdl2 0.38 This fixes crashes when using the sdl3 compat layer for sdl2. --- .github/workflows/setup-sdl.ps1 | 20 ++++++++++---------- NEWS.md | 2 ++ sdl/Cargo.toml | 2 +- sdl/src/font.rs | 16 +++------------- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/.github/workflows/setup-sdl.ps1 b/.github/workflows/setup-sdl.ps1 index c0418490..e792ec8e 100644 --- a/.github/workflows/setup-sdl.ps1 +++ b/.github/workflows/setup-sdl.ps1 @@ -14,22 +14,22 @@ # under the License. Invoke-WebRequest ` - -Uri https://www.libsdl.org/release/SDL2-devel-2.26.1-VC.zip ` - -OutFile SDL2-devel-2.26.1-VC.zip + -Uri https://www.libsdl.org/release/SDL2-devel-2.32.10-VC.zip ` + -OutFile SDL2-devel-2.32.10-VC.zip Invoke-WebRequest ` - -Uri https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-devel-2.0.15-VC.zip ` - -OutFile SDL2_ttf-devel-2.0.15-VC.zip + -Uri https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-devel-2.24.0-VC.zip ` + -OutFile SDL2_ttf-devel-2.24.0-VC.zip -unzip SDL2-devel-2.26.1-VC.zip -unzip SDL2_ttf-devel-2.0.15-VC.zip +unzip SDL2-devel-2.32.10-VC.zip +unzip SDL2_ttf-devel-2.24.0-VC.zip [void](New-Item -Force -Type Directory libs) -Copy-Item .\SDL2-2.26.1\lib\x64\*.lib,.\SDL2_ttf-2.0.15\lib\x64\*.lib libs +Copy-Item .\SDL2-2.32.10\lib\x64\*.lib,.\SDL2_ttf-2.24.0\lib\x64\*.lib libs [void](New-Item -Force -Type Directory dlls) -Copy-Item .\SDL2-2.26.1\lib\x64\*.dll,.\SDL2_ttf-2.0.15\lib\x64\*.dll dlls -Copy-Item .\SDL2-2.26.1\lib\x64\*.txt,.\SDL2_ttf-2.0.15\lib\x64\*.txt dlls +Copy-Item .\SDL2-2.32.10\lib\x64\*.dll,.\SDL2_ttf-2.24.0\lib\x64\*.dll dlls +Copy-Item .\SDL2-2.32.10\lib\x64\*.txt,.\SDL2_ttf-2.24.0\lib\x64\*.txt dlls -Remove-Item -Recurse -Force .\SDL2-2.26.1,.\SDL2_ttf-2.0.15,SDL2*.zip +Remove-Item -Recurse -Force .\SDL2-2.32.10,.\SDL2_ttf-2.24.0,SDL2*.zip foreach ($dir in ".", diff --git a/NEWS.md b/NEWS.md index 7e176c16..304313a2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -55,6 +55,8 @@ STILL UNDER DEVELOPMENT; NOT RELEASED YET. * Added support for `EXIT FUNCTION`, `EXIT FOR`, and `EXIT SUB`. +* Fixed crashes when using the SDL 3 compatibility bindings for SDL 2. + ## Changes in version 0.11.1 **Released on 2024-09-14.** diff --git a/sdl/Cargo.toml b/sdl/Cargo.toml index 07f2b981..beedd6bd 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -26,7 +26,7 @@ version = "0.11.99" # ENDBASIC-VERSION path = "../std" [dependencies.sdl2] -version = "0.36" +version = "0.38" default-features = false features = ["ttf"] diff --git a/sdl/src/font.rs b/sdl/src/font.rs index 7455d455..76c252ae 100644 --- a/sdl/src/font.rs +++ b/sdl/src/font.rs @@ -18,7 +18,7 @@ use crate::string_error_to_io_error; use endbasic_std::console::{CharsXY, SizeInPixels}; use once_cell::sync::Lazy; -use sdl2::ttf::{Font, FontError, InitError, Sdl2TtfContext}; +use sdl2::ttf::{Font, FontError, Sdl2TtfContext}; use std::convert::TryFrom; use std::io; use std::path::Path; @@ -27,17 +27,7 @@ use std::path::Path; /// fonts seems to be incredibly hard because of how we hide the `SdlConsole` implementation behind /// the `Console` trait. It might be possible to do this in a better way, but for now, keeping the /// context global works well and is simple enough. -static TTF_CONTEXT: Lazy> = Lazy::new(sdl2::ttf::init); - -/// Converts an `InitError` to an `io::Error`. -fn init_error_to_io_error(e: &'static InitError) -> io::Error { - match e { - InitError::AlreadyInitializedError => { - panic!("Initialization from once_cell should happen only once") - } - InitError::InitializationError(e) => io::Error::new(e.kind(), format!("{}", e)), - } -} +static TTF_CONTEXT: Lazy> = Lazy::new(sdl2::ttf::init); /// Converts a `FontError` to an `io::Error`. pub(crate) fn font_error_to_io_error(e: FontError) -> io::Error { @@ -58,7 +48,7 @@ impl<'a> MonospacedFont<'a> { /// Loads the font from the file `path` with `point_size`. If the loaded font is not /// monospaced, returns an error. pub(crate) fn load(path: &Path, point_size: u16) -> io::Result> { - let ttf_context = TTF_CONTEXT.as_ref().map_err(init_error_to_io_error)?; + let ttf_context = TTF_CONTEXT.as_ref().map_err(|s| io::Error::other(s.to_string()))?; let font = ttf_context.load_font(path, point_size).map_err(string_error_to_io_error)?; From 8aad0bd9e1e492046b7c19c41f60523beccaf73d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 6 Jan 2026 13:17:06 +0100 Subject: [PATCH 066/110] Don't make CloudService Cloneable This was put in place for tests but is not necessary and makes further changes for automatic file cleanup harder. --- client/src/cloud.rs | 76 ++++++++++++++++++++--------------------- client/src/testutils.rs | 2 +- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/client/src/cloud.rs b/client/src/cloud.rs index ede5eeb7..05a668d8 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -88,7 +88,6 @@ struct AuthData { } /// An implementation of the EndBASIC service client that talks to a remote server. -#[cfg_attr(test, derive(Clone))] pub struct CloudService { api_address: Url, client: reqwest::Client, @@ -395,7 +394,7 @@ mod testutils { /// Holds state for a test and allows for automatic cleanup of shared resources. pub(crate) struct TestContext { - service: CloudService, + pub(super) service: CloudService, // State required to automatically clean up files on `drop`. username: Option, @@ -408,11 +407,6 @@ mod testutils { TestContext { service: new_service_from_env(), username: None, files_to_delete: vec![] } } - /// Returns the service client in this context. - pub(crate) fn service(&self) -> CloudService { - self.service.clone() - } - /// Returns the username of the selected test account. pub(crate) fn get_username(&self, i: u8) -> String { env::var(format!("TEST_ACCOUNT_{}_USERNAME", i)).expect("Expected env config not found") @@ -521,7 +515,6 @@ mod tests { #[tokio::main] async fn run(context: &mut TestContext) { let username = context.do_login(1).await; - let mut service = context.service(); let mut needed_bytes = 0; let mut needed_files = 0; @@ -534,7 +527,7 @@ mod tests { filenames_and_contents.push((filename, content)); } - let response = service.get_files(&username).await.unwrap(); + let response = context.service.get_files(&username).await.unwrap(); for (filename, _content) in &filenames_and_contents { assert!(!response.files.iter().any(|x| &x.filename == filename)); } @@ -546,15 +539,19 @@ mod tests { assert!(disk_free.files() >= needed_files, "Not enough space for test run"); for (filename, _content) in &filenames_and_contents { - let err = service.get_file(&username, filename).await.unwrap_err(); + let err = context.service.get_file(&username, filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); } for (filename, content) in &filenames_and_contents { - service.patch_file_content(&username, filename, content.clone()).await.unwrap(); + context + .service + .patch_file_content(&username, filename, content.clone()) + .await + .unwrap(); } - let response = service.get_files(&username).await.unwrap(); + let response = context.service.get_files(&username).await.unwrap(); for (filename, _content) in &filenames_and_contents { assert!(response.files.iter().any(|x| &x.filename == filename)); } @@ -568,11 +565,10 @@ mod tests { content: B, ) { let username = context.do_login(1).await; - let mut service = context.service(); let content = content.into(); - service.patch_file_content(&username, filename, content.clone()).await.unwrap(); - assert_eq!(content, service.get_file(&username, filename).await.unwrap()); + context.service.patch_file_content(&username, filename, content.clone()).await.unwrap(); + assert_eq!(content, context.service.get_file(&username, filename).await.unwrap()); } #[test] @@ -615,10 +611,9 @@ mod tests { #[tokio::main] async fn run(context: &mut TestContext) { let username = context.do_login(1).await; - let mut service = context.service(); let (filename, _content) = context.random_file(); - let err = service.get_file(&username, &filename).await.unwrap_err(); + let err = context.service.get_file(&username, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); } run(&mut TestContext::new_from_env()); @@ -629,14 +624,14 @@ mod tests { fn test_patch_file_without_login() { #[tokio::main] async fn run(context: &mut TestContext) { - let mut service = context.service(); let username = context.get_username(1); context.do_login(1).await; let (filename, _content) = context.random_file(); context.do_logout().await; - let err = service + let err = context + .service .patch_file_content(&username, &filename, b"foo".to_vec()) .await .unwrap_err(); @@ -651,8 +646,6 @@ mod tests { fn test_acls_private() { #[tokio::main] async fn run(context: &mut TestContext) { - let mut service = context.service(); - let (filename, content) = context.random_file(); let username1 = context.get_username(1); @@ -660,16 +653,21 @@ mod tests { // Share username1's file with username2. context.do_login(1).await; - service.patch_file_content(&username1, &filename, content.clone()).await.unwrap(); + context + .service + .patch_file_content(&username1, &filename, content.clone()) + .await + .unwrap(); // Read username1's file as username2 before it is shared. context.do_login(2).await; - let err = service.get_file(&username1, &filename).await.unwrap_err(); + let err = context.service.get_file(&username1, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); // Share username1's file with username2. context.do_login(1).await; - service + context + .service .patch_file_acls( &username1, &filename, @@ -681,7 +679,7 @@ mod tests { // Read username1's file as username2 again, now that it is shared. context.do_login(2).await; - let response = service.get_file(&username1, &filename).await.unwrap(); + let response = context.service.get_file(&username1, &filename).await.unwrap(); assert_eq!(content, response); } run(&mut TestContext::new_from_env()); @@ -692,24 +690,27 @@ mod tests { fn test_acls_public() { #[tokio::main] async fn run(context: &mut TestContext) { - let mut service = context.service(); - let (filename, content) = context.random_file(); let username1 = context.get_username(1); // Share username1's file with the public. context.do_login(1).await; - service.patch_file_content(&username1, &filename, content.clone()).await.unwrap(); + context + .service + .patch_file_content(&username1, &filename, content.clone()) + .await + .unwrap(); // Read username1's file as a guest before it is shared. context.do_logout().await; - let err = service.get_file(&username1, &filename).await.unwrap_err(); + let err = context.service.get_file(&username1, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); // Share username1's file with the public. context.do_login(1).await; - service + context + .service .patch_file_acls( &username1, &filename, @@ -721,7 +722,7 @@ mod tests { // Read username1's file as a guest again, now that it is shared. context.do_logout().await; - let response = service.get_file(&username1, &filename).await.unwrap(); + let response = context.service.get_file(&username1, &filename).await.unwrap(); assert_eq!(content, response); } run(&mut TestContext::new_from_env()); @@ -733,14 +734,13 @@ mod tests { #[tokio::main] async fn run(context: &mut TestContext) { let username = context.do_login(1).await; - let mut service = context.service(); let (filename, content) = context.random_file(); - service.patch_file_content(&username, &filename, content).await.unwrap(); + context.service.patch_file_content(&username, &filename, content).await.unwrap(); - service.delete_file(&username, &filename).await.unwrap(); + context.service.delete_file(&username, &filename).await.unwrap(); - let err = service.get_file(&username, &filename).await.unwrap_err(); + let err = context.service.get_file(&username, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); assert!(format!("{}", err).contains("(server code: 404")); } @@ -753,10 +753,9 @@ mod tests { #[tokio::main] async fn run(context: &mut TestContext) { let username = context.do_login(1).await; - let mut service = context.service(); let (filename, _content) = context.random_file(); - let err = service.delete_file(&username, &filename).await.unwrap_err(); + let err = context.service.delete_file(&username, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::NotFound, err.kind(), "{}", err); assert!(format!("{}", err).contains("(server code: 404")); } @@ -768,14 +767,13 @@ mod tests { fn test_delete_file_without_login() { #[tokio::main] async fn run(context: &mut TestContext) { - let mut service = context.service(); let username = context.get_username(1); context.do_login(1).await; let (filename, _content) = context.random_file(); context.do_logout().await; - let err = service.delete_file(&username, &filename).await.unwrap_err(); + let err = context.service.delete_file(&username, &filename).await.unwrap_err(); assert_eq!(io::ErrorKind::PermissionDenied, err.kind(), "{}", err); assert!(format!("{}", err).contains("Not logged in")); } diff --git a/client/src/testutils.rs b/client/src/testutils.rs index e818a5b1..6d2faa30 100644 --- a/client/src/testutils.rs +++ b/client/src/testutils.rs @@ -325,7 +325,7 @@ impl ClientTester { } /// See the wrapped `Tester::run` function for details. - pub(crate) fn run>(&mut self, script: S) -> ClientChecker { + pub(crate) fn run>(&mut self, script: S) -> ClientChecker<'_> { let checker = self.tester.run(script); ClientChecker { checker, service: self.service.clone(), exp_access_token: None } } From ba1ebcf6322a3ba21057a3725af2dee7926bc22e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 6 Jan 2026 13:12:11 +0100 Subject: [PATCH 067/110] Fix auto-deletion of temp cloud files in tests Implement an interceptor for the CloudService so that we can correctly track which files are created and delete them with error reporting during teardown instead of ignoring errors. This helps surface cases where temp files were not correctly deleted and ended up causing test flakiness due to test account quota exhaustion. --- client/src/cloud.rs | 208 +++++++++++++++++++++++++++++++++----------- 1 file changed, 159 insertions(+), 49 deletions(-) diff --git a/client/src/cloud.rs b/client/src/cloud.rs index 05a668d8..8749e1f1 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -384,27 +384,167 @@ impl Service for CloudService { #[cfg(test)] mod testutils { use super::*; + use std::collections::HashMap; use std::env; + /// Wraps a `Service` to auto-delete created files. + pub struct AutoDeletingService { + /// The wrapped service. + service: S, + + /// Current credentials a (username, password) pair. + current_user: Option<(String, String)>, + + /// List of files to delete as a mapping of filename to the credentials required for deletion. + files_to_delete: HashMap, + } + + impl AutoDeletingService { + /// Creates a new auto-deleting service that wraps `service`. + pub fn new(service: S) -> Self { + Self { service, current_user: None, files_to_delete: HashMap::default() } + } + } + + #[async_trait(?Send)] + impl Service for AutoDeletingService { + async fn signup(&mut self, request: &SignupRequest) -> io::Result<()> { + self.service.signup(request).await + } + + async fn login(&mut self, username: &str, password: &str) -> io::Result { + let result = self.service.login(username, password).await; + if result.is_ok() { + self.current_user = Some((username.to_owned(), password.to_owned())); + } + result + } + + async fn logout(&mut self) -> io::Result<()> { + let result = self.service.logout().await; + if result.is_ok() { + self.current_user = None; + } + result + } + + fn is_logged_in(&self) -> bool { + self.service.is_logged_in() + } + + fn logged_in_username(&self) -> Option { + self.service.logged_in_username() + } + + async fn get_files(&mut self, username: &str) -> io::Result { + self.service.get_files(username).await + } + + async fn get_file(&mut self, username: &str, filename: &str) -> io::Result> { + self.service.get_file(username, filename).await + } + + async fn get_file_acls(&mut self, username: &str, filename: &str) -> io::Result { + self.service.get_file_acls(username, filename).await + } + + async fn patch_file_content( + &mut self, + username: &str, + filename: &str, + content: Vec, + ) -> io::Result<()> { + let result = self.service.patch_file_content(username, filename, content).await; + if result.is_ok() { + self.files_to_delete + .insert(filename.to_owned(), self.current_user.clone().unwrap()); + } + result + } + + async fn patch_file_acls( + &mut self, + username: &str, + filename: &str, + add: &FileAcls, + remove: &FileAcls, + ) -> io::Result<()> { + self.service.patch_file_acls(username, filename, add, remove).await + } + + async fn delete_file(&mut self, username: &str, filename: &str) -> io::Result<()> { + let result = self.service.delete_file(username, filename).await; + if result.is_ok() { + self.files_to_delete.remove(filename); + } + result + } + } + + impl Drop for AutoDeletingService { + fn drop(&mut self) { + #[tokio::main] + #[allow(clippy::single_match)] + async fn cleanup(service: &mut AutoDeletingService) { + if let Some((username, _password)) = service.current_user.as_ref() { + service + .service + .logout() + .await + .map_err(|e| { + format!("Failed to log out for {} during cleanup: {}", username, e) + }) + .unwrap(); + } + + for (filename, (username, password)) in service.files_to_delete.iter() { + service + .service + .login(username, password) + .await + .map_err(|e| { + format!("Failed to log in for {} during cleanup: {}", username, e) + }) + .unwrap(); + + service + .service + .delete_file(username, filename) + .await + .map_err(|e| { + format!("Failed to delete file {} during cleanup: {}", filename, e) + }) + .unwrap(); + + service + .service + .logout() + .await + .map_err(|e| { + format!("Failed to log out for {} during cleanup: {}", username, e) + }) + .unwrap(); + } + } + cleanup(self); + } + } + /// Creates a new service that talks to the configured API service for testing. - pub(crate) fn new_service_from_env() -> CloudService { + pub(crate) fn new_service_from_env() -> AutoDeletingService { let service_api = env::var("SERVICE_URL").expect("Expected env config not found"); - CloudService::new(&service_api).unwrap() + AutoDeletingService::new(CloudService::new(&service_api).unwrap()) } /// Holds state for a test and allows for automatic cleanup of shared resources. pub(crate) struct TestContext { - pub(super) service: CloudService, - - // State required to automatically clean up files on `drop`. - username: Option, - files_to_delete: Vec, + pub(super) service: AutoDeletingService, } impl TestContext { /// Creates a new test context that talks to the configured API service. pub(crate) fn new_from_env() -> Self { - TestContext { service: new_service_from_env(), username: None, files_to_delete: vec![] } + TestContext { service: new_service_from_env() } } /// Returns the username of the selected test account. @@ -422,14 +562,12 @@ mod testutils { let password = env::var(format!("TEST_ACCOUNT_{}_PASSWORD", i)) .expect("Expected env config not found"); let _response = self.service.login(&username, &password).await.unwrap(); - self.username = Some(username.clone()); username } /// Clears the authentication token to represent a log out. pub(crate) async fn do_logout(&mut self) { self.service.logout().await.unwrap(); - self.username = None; } /// Generates a random filename and its content for testing, and makes sure the file gets @@ -437,41 +575,9 @@ mod testutils { pub(crate) fn random_file(&mut self) -> (String, Vec) { let filename = format!("file-{}", rand::random::()); let content = format!("Test content for {}", filename); - self.files_to_delete.push(filename.clone()); (filename, content.into_bytes()) } } - - impl Drop for TestContext { - fn drop(&mut self) { - #[tokio::main] - #[allow(clippy::single_match)] - async fn cleanup(context: &mut TestContext) { - match context.username.as_ref() { - Some(username) => { - for filename in context.files_to_delete.iter() { - if let Err(e) = context.service.delete_file(username, filename).await { - eprintln!( - "Failed to delete file {} during cleanup: {}", - filename, e - ); - } - } - - if let Err(e) = context.service.logout().await { - eprintln!("Failed to log out for {} during cleanup: {}", username, e); - } - } - _ => { - // Nothing to do: if we have a file in context.files_to_delete, it might be - // because we generated its name before authenticating or logging in, but - // in that case, we can't have created the file. - } - } - } - cleanup(self); - } - } } #[cfg(test)] @@ -498,15 +604,19 @@ mod tests { run(&mut TestContext::new_from_env()); } - #[tokio::test] + #[test] #[ignore = "Requires environment configuration and is expensive"] - async fn test_login_bad_password() { - let username = env::var("TEST_ACCOUNT_1_USERNAME").expect("Expected env config not found"); - let password = "this is an invalid password for the test account"; + fn test_login_bad_password() { + #[tokio::main] + async fn run(context: &mut TestContext) { + let username = + env::var("TEST_ACCOUNT_1_USERNAME").expect("Expected env config not found"); + let password = "this is an invalid password for the test account"; - let mut service = new_service_from_env(); - let err = service.login(&username, &password).await.unwrap_err(); - assert_eq!(io::ErrorKind::PermissionDenied, err.kind()); + let err = context.service.login(&username, &password).await.unwrap_err(); + assert_eq!(io::ErrorKind::PermissionDenied, err.kind()); + } + run(&mut TestContext::new_from_env()); } #[test] From 31ce5419dec719b400729d6d81b9fefc77818591 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 8 Jan 2026 04:14:54 +0100 Subject: [PATCH 068/110] Remove dead code in compile_expr I'm not sure when compile_expr used to support compiling references but that codepath is never taken now. Remove it along with all of its supporting code. --- core/src/compiler/exprs.rs | 58 -------------------------------------- core/src/compiler/mod.rs | 24 +++++----------- 2 files changed, 7 insertions(+), 75 deletions(-) diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 3350aa7a..eb6d3610 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -370,47 +370,6 @@ fn compile_expr_symbol( Ok(vtype) } -/// Compiles the load of a symbol in the context of a command argument. -fn compile_expr_symbol_ref( - instrs: &mut Vec, - symtable: &mut SymbolsTable, - span: SymbolSpan, -) -> Result { - let key = SymbolKey::from(span.vref.name()); - match symtable.get(&key) { - None => { - let vtype = span.vref.ref_type().unwrap_or(ExprType::Integer); - - if !span.vref.accepts(vtype) { - return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); - } - - symtable.insert(key.clone(), SymbolPrototype::Variable(vtype)); - instrs.push(Instruction::LoadRef(key, vtype, span.pos)); - Ok(vtype) - } - - Some(SymbolPrototype::Array(vtype, _)) | Some(SymbolPrototype::Variable(vtype)) => { - let vtype = *vtype; - - if !span.vref.accepts(vtype) { - return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); - } - - instrs.push(Instruction::LoadRef(key, vtype, span.pos)); - Ok(vtype) - } - - Some(SymbolPrototype::Callable(md)) => { - if !span.vref.accepts_callable(md.return_type()) { - return Err(Error::IncompatibleTypeAnnotationInReference(span.pos, span.vref)); - } - - Err(Error::NotArrayOrFunction(span.pos, key)) - } - } -} - /// Compiles an array access. fn compile_array_ref( instrs: &mut Vec, @@ -693,23 +652,6 @@ pub(super) fn compile_expr( } } -/// Compiles the evaluation of an expression, appends its instructions to `instrs`, and returns -/// the type of the compiled expression. -/// -/// This function should be used only when compiling the arguments to a builtin command, because -/// in that context, we need mutable access to the `symtable`in order to define output variables -/// (if any). -pub(super) fn compile_expr_in_command( - instrs: &mut Vec, - symtable: &mut SymbolsTable, - expr: Expr, -) -> Result { - match expr { - Expr::Symbol(span) => compile_expr_symbol_ref(instrs, symtable, span), - expr => compile_expr(instrs, symtable, expr, false), - } -} - /// Compiles a single expression, expecting it to be of a `target` type. Applies casts if /// possible. pub(super) fn compile_expr_as_type( diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 780ee00b..919ec205 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -29,7 +29,7 @@ use std::io; mod args; pub use args::*; mod exprs; -use exprs::{compile_expr, compile_expr_as_type, compile_expr_in_command}; +use exprs::{compile_expr, compile_expr_as_type}; /// Compilation errors. #[derive(Debug, thiserror::Error)] @@ -423,7 +423,7 @@ impl Compiler { return Err(Error::IncompatibleTypeAnnotationInReference(span.vref_pos, span.vref)); } - let etype = self.compile_expr(span.expr, false)?; + let etype = self.compile_expr(span.expr)?; let etype = self.maybe_cast(atype, etype); if etype != atype { return Err(Error::IncompatibleTypesInAssignment(span.vref_pos, etype, atype)); @@ -443,7 +443,7 @@ impl Compiler { /// 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 etype = self.compile_expr(expr, false)?; + let etype = self.compile_expr(expr)?; if let Some(current_callable) = self.current_callable.as_ref() { if key == current_callable.0 { @@ -626,7 +626,7 @@ impl Compiler { self.compile_assignment(span.iter.clone(), span.iter_pos, span.start)?; let start_pc = self.next_pc; - let end_etype = self.compile_expr(span.end, false)?; + let end_etype = self.compile_expr(span.end)?; match end_etype { ExprType::Boolean => (), _ => panic!("Synthesized end condition for FOR must yield a boolean"), @@ -813,18 +813,8 @@ impl Compiler { /// Compiles the evaluation of an expression, appends its instructions to the /// compilation context, and returns the type of the compiled expression. - /// - /// `allow_varrefs` should be true for top-level expression compilations within - /// function arguments. In that specific case, we must leave bare variable - /// references unevaluated because we don't know if the function wants to take - /// the reference or the value. - fn compile_expr(&mut self, expr: Expr, allow_varrefs: bool) -> Result { - let result = if allow_varrefs { - compile_expr_in_command(&mut self.instrs, &mut self.symtable, expr) - } else { - compile_expr(&mut self.instrs, &self.symtable, expr, false) - }; - match result { + fn compile_expr(&mut self, expr: Expr) -> Result { + match compile_expr(&mut self.instrs, &self.symtable, expr, false) { Ok(result) => { self.next_pc = self.instrs.len(); Ok(result) @@ -845,7 +835,7 @@ impl Compiler { /// expression is invalid or if it does not evaluate to a boolean. fn compile_expr_guard>(&mut self, guard: Expr, errmsg: S) -> Result<()> { let pos = guard.start_pos(); - match self.compile_expr(guard, false)? { + match self.compile_expr(guard)? { ExprType::Boolean => Ok(()), _ => Err(Error::NotABooleanCondition(pos, errmsg.into())), } From b683cae8c52cc3c48f98453c072da0af2f507c61 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 8 Jan 2026 04:17:00 +0100 Subject: [PATCH 069/110] Remove support for callables in LoadRef I suppose LoadRef used to evaluate function calls at some point, but it doesn't now. Remove this codepath. --- core/src/exec.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/core/src/exec.rs b/core/src/exec.rs index 02e07afc..ad84e1c6 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1419,18 +1419,6 @@ impl Machine { } Instruction::LoadRef(key, etype, pos) => { - let sym = self.symbols.load(key); - if let Some(Symbol::Callable(f)) = sym { - if f.metadata().is_argless() { - return Ok(InternalStopReason::Upcall(UpcallData { - name: key.clone(), - return_type: Some(*etype), - pos: *pos, - nargs: 0, - })); - } - }; - context.value_stack.push_varref(key.clone(), *etype, *pos); context.pc += 1; } From cbb0a43917dfcc0c13fa855d88033c8159e1969b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Wed, 7 Jan 2026 21:13:20 -0800 Subject: [PATCH 070/110] Add BuiltinCallISpan and FunctionCallISpan This is to give the various arguments a name, because I'm about to add another usize to replace the names and that would become confusing due to the presence of another usize field. --- core/src/bytecode.rs | 59 ++++++++++--- core/src/compiler/exprs.rs | 41 +++++++-- core/src/compiler/mod.rs | 177 ++++++++++++++++++++++++++++++++----- core/src/exec.rs | 18 ++-- 4 files changed, 244 insertions(+), 51 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index b4e101dc..dc719fd4 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -22,6 +22,23 @@ use crate::syms::SymbolKey; /// Convenience type to represent a program address. pub type Address = usize; +/// Components of a builtin command call. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct BuiltinCallISpan { + /// Name of the builtin to call. + pub name: SymbolKey, + + /// Position of the name. + pub name_pos: LineCol, + + /// Number of arguments on the stack for the call. + /// + /// The arguments in the stack are interspersed with the separators used to separate them from + /// each other because those separators have meaning. + pub nargs: usize, +} + /// Components of a variable definition. #[derive(Debug, PartialEq)] #[cfg_attr(test, derive(Clone))] @@ -59,6 +76,23 @@ pub struct DimArrayISpan { pub subtype_pos: LineCol, } +/// Components of a builtin function call. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct FunctionCallISpan { + /// Name of the builtin to call. + pub name: SymbolKey, + + /// Position of the name. + pub name_pos: LineCol, + + /// Return type of the function. + pub return_type: ExprType, + + /// Number of arguments on the stack for the call. + pub nargs: usize, +} + /// Components of an unconditional jump instruction. #[derive(Debug, Eq, PartialEq)] pub struct JumpISpan { @@ -248,16 +282,13 @@ pub enum Instruction { Assign(SymbolKey), /// Represents a call to a builtin command such as `PRINT` with the given number of arguments. - /// - /// The arguments in the stack are interspersed with the separators used to separate them from. - /// each other, because those separators have meaning. - BuiltinCall(SymbolKey, LineCol, usize), + BuiltinCall(BuiltinCallISpan), /// Represents an unconditional call to a location that will return. Call(JumpISpan), /// Represents a call to the given function with the given number of arguments. - FunctionCall(SymbolKey, ExprType, LineCol, usize), + FunctionCall(FunctionCallISpan), /// Represents a variable definition. Dim(DimISpan), @@ -402,20 +433,20 @@ impl Instruction { Instruction::Assign(key) => ("SETV", Some(key.to_string())), - Instruction::BuiltinCall(key, _pos, nargs) => { - ("CALLB", Some(format!("{}, {}", key, nargs))) + Instruction::BuiltinCall(span) => { + ("CALLB", Some(format!("{}, {}", span.name, span.nargs))) } Instruction::Call(span) => ("CALLA", Some(format!("{:04x}", span.addr))), - Instruction::FunctionCall(key, etype, _pos, nargs) => { - let opcode = match etype { + Instruction::FunctionCall(span) => { + let opcode = match span.return_type { ExprType::Boolean => "CALLF?", ExprType::Double => "CALLF#", ExprType::Integer => "CALLF%", ExprType::Text => "CALLF$", }; - (opcode, Some(format!("{}, {}", key, nargs))) + (opcode, Some(format!("{}, {}", span.name, span.nargs))) } Instruction::Dim(span) => { @@ -551,9 +582,9 @@ impl Instruction { Instruction::ArrayAssignment(_, pos, _) => Some(*pos), Instruction::ArrayLoad(_, pos, _) => Some(*pos), Instruction::Assign(_) => None, - Instruction::BuiltinCall(_, pos, _) => Some(*pos), + Instruction::BuiltinCall(span) => Some(span.name_pos), Instruction::Call(_) => None, - Instruction::FunctionCall(_, _, pos, _) => Some(*pos), + Instruction::FunctionCall(span) => Some(span.name_pos), Instruction::Dim(_) => None, Instruction::DimArray(span) => Some(span.name_pos), Instruction::End(_) => None, @@ -634,7 +665,7 @@ impl Instruction { | Instruction::PowerIntegers(_) | Instruction::NegateInteger(_) | Instruction::ConcatStrings(_) - | Instruction::FunctionCall(_, _, _, _) + | Instruction::FunctionCall(_) | Instruction::DoubleToInteger | Instruction::IntegerToDouble | Instruction::LoadBoolean(_, _) @@ -651,7 +682,7 @@ impl Instruction { Instruction::ArrayAssignment(_, _, _) | Instruction::Assign(_) - | Instruction::BuiltinCall(_, _, _) + | Instruction::BuiltinCall(_) | Instruction::Call(_) | Instruction::Dim(_) | Instruction::DimArray(_) diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index eb6d3610..900be859 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -360,7 +360,15 @@ fn compile_expr_symbol( let nargs = compile_function_args(md, instrs, symtable, span.pos, vec![])?; debug_assert_eq!(0, nargs, "Argless compiler must have returned zero arguments"); - (Instruction::FunctionCall(key, etype, span.pos, 0), etype) + ( + Instruction::FunctionCall(FunctionCallISpan { + name: key, + name_pos: span.pos, + return_type: etype, + nargs: 0, + }), + etype, + ) } }; if !span.vref.accepts(vtype) { @@ -638,7 +646,12 @@ pub(super) fn compile_expr( let span_pos = span.vref_pos; let nargs = compile_function_args(md, instrs, symtable, span_pos, span.args)?; - instrs.push(Instruction::FunctionCall(key, vtype, span_pos, nargs)); + instrs.push(Instruction::FunctionCall(FunctionCallISpan { + name: key, + name_pos: span_pos, + return_type: vtype, + nargs, + })); Ok(vtype) } @@ -686,6 +699,7 @@ pub(super) fn compile_expr_as_type( #[cfg(test)] mod tests { use super::*; + use crate::bytecode::{BuiltinCallISpan, FunctionCallISpan}; use crate::compiler::{ testutils::*, ArgSepSyntax, RepeatedSyntax, RepeatedTypeSyntax, RequiredRefSyntax, RequiredValueSyntax, SingularArgSyntax, @@ -728,7 +742,12 @@ mod tests { .compile() .expect_instr( 0, - Instruction::FunctionCall(SymbolKey::from("f"), ExprType::Integer, lc(1, 5), 0), + Instruction::FunctionCall(FunctionCallISpan { + name: SymbolKey::from("f"), + name_pos: lc(1, 5), + return_type: ExprType::Integer, + nargs: 0, + }), ) .expect_instr(1, Instruction::Assign(SymbolKey::from("i"))) .check(); @@ -869,7 +888,14 @@ mod tests { 0, Instruction::LoadRef(SymbolKey::from("a"), ExprType::Integer, lc(1, 3)), ) - .expect_instr(1, Instruction::BuiltinCall(SymbolKey::from("C"), lc(1, 1), 1)) + .expect_instr( + 1, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("C"), + name_pos: lc(1, 1), + nargs: 1, + }), + ) .check(); } @@ -1194,7 +1220,12 @@ mod tests { .expect_instr(6, Instruction::PushInteger(3, lc(1, 9))) .expect_instr( 7, - Instruction::FunctionCall(SymbolKey::from("FOO"), ExprType::Integer, lc(1, 5), 3), + Instruction::FunctionCall(FunctionCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(1, 5), + return_type: ExprType::Integer, + nargs: 3, + }), ) .expect_instr(8, Instruction::Assign(SymbolKey::from("i"))) .check(); diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 919ec205..eef19206 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -878,7 +878,11 @@ impl Compiler { span.args, )?; self.next_pc = self.instrs.len(); - self.emit(Instruction::BuiltinCall(key, span.vref_pos, nargs)); + self.emit(Instruction::BuiltinCall(BuiltinCallISpan { + name: key, + name_pos: span.vref_pos, + nargs, + })); } Statement::Callable(span) => { @@ -1125,14 +1129,14 @@ impl Compiler { for instr in &mut self.instrs { match instr { - Instruction::BuiltinCall(key, _, _) => { - if let Some(addr) = subs.get(key) { + Instruction::BuiltinCall(span) => { + if let Some(addr) = subs.get(&span.name) { *instr = Instruction::Call(JumpISpan { addr: *addr }); } } - Instruction::FunctionCall(key, _, _, _) => { - if let Some(addr) = functions.get(key) { + Instruction::FunctionCall(span) => { + if let Some(addr) = functions.get(&span.name) { *instr = Instruction::Call(JumpISpan { addr: *addr }); } } @@ -1348,6 +1352,7 @@ mod testutils { mod tests { use super::testutils::*; use super::*; + use crate::bytecode::{BuiltinCallISpan, DimArrayISpan}; use crate::syms::CallableMetadataBuilder; use std::borrow::Cow; @@ -1556,7 +1561,14 @@ mod tests { .define_callable(CallableMetadataBuilder::new("CMD")) .parse("CMD") .compile() - .expect_instr(0, Instruction::BuiltinCall(SymbolKey::from("CMD"), lc(1, 1), 0)) + .expect_instr( + 0, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("CMD"), + name_pos: lc(1, 1), + nargs: 0, + }), + ) .check(); } @@ -1579,7 +1591,14 @@ mod tests { .expect_instr(1, Instruction::JumpIfNotTrue(5)) .expect_instr(2, Instruction::PushInteger(2, lc(1, 22))) .expect_instr(3, Instruction::PushInteger(1, lc(1, 19))) - .expect_instr(4, Instruction::BuiltinCall(SymbolKey::from("CMD"), lc(1, 15), 2)) + .expect_instr( + 4, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("CMD"), + name_pos: lc(1, 15), + nargs: 2, + }), + ) .check(); } @@ -1843,7 +1862,14 @@ mod tests { .define_callable(CallableMetadataBuilder::new("FOO")) .parse("DO\nFOO\nLOOP") .compile() - .expect_instr(0, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(2, 1), 0)) + .expect_instr( + 0, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(2, 1), + nargs: 0, + }), + ) .expect_instr(1, Instruction::Jump(JumpISpan { addr: 0 })) .check(); } @@ -1856,7 +1882,14 @@ mod tests { .compile() .expect_instr(0, Instruction::PushBoolean(true, lc(1, 10))) .expect_instr(1, Instruction::JumpIfNotTrue(4)) - .expect_instr(2, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(2, 1), 0)) + .expect_instr( + 2, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(2, 1), + nargs: 0, + }), + ) .expect_instr(3, Instruction::Jump(JumpISpan { addr: 0 })) .check(); } @@ -1867,7 +1900,14 @@ mod tests { .define_callable(CallableMetadataBuilder::new("FOO")) .parse("DO\nFOO\nLOOP WHILE TRUE") .compile() - .expect_instr(0, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(2, 1), 0)) + .expect_instr( + 0, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(2, 1), + nargs: 0, + }), + ) .expect_instr(1, Instruction::PushBoolean(true, lc(3, 12))) .expect_instr(2, Instruction::JumpIfTrue(0)) .check(); @@ -2448,7 +2488,14 @@ mod tests { .define_callable(CallableMetadataBuilder::new("FOO")) .parse("@sub\nFOO\nRETURN\nGOSUB @sub") .compile() - .expect_instr(0, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(2, 1), 0)) + .expect_instr( + 0, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(2, 1), + nargs: 0, + }), + ) .expect_instr(1, Instruction::Return(lc(3, 1))) .expect_instr(2, Instruction::Call(JumpISpan { addr: 0 })) .check(); @@ -2471,7 +2518,14 @@ mod tests { .compile() .expect_instr(0, Instruction::PushBoolean(false, lc(1, 4))) .expect_instr(1, Instruction::JumpIfNotTrue(3)) - .expect_instr(2, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(1, 16), 0)) + .expect_instr( + 2, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(1, 16), + nargs: 0, + }), + ) .check(); } @@ -2485,15 +2539,36 @@ mod tests { .compile() .expect_instr(0, Instruction::PushBoolean(false, lc(1, 4))) .expect_instr(1, Instruction::JumpIfNotTrue(4)) - .expect_instr(2, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(2, 1), 0)) + .expect_instr( + 2, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(2, 1), + nargs: 0, + }), + ) .expect_instr(3, Instruction::Jump(JumpISpan { addr: 11 })) .expect_instr(4, Instruction::PushBoolean(true, lc(3, 8))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) - .expect_instr(6, Instruction::BuiltinCall(SymbolKey::from("BAR"), lc(4, 1), 0)) + .expect_instr( + 6, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("BAR"), + name_pos: lc(4, 1), + nargs: 0, + }), + ) .expect_instr(7, Instruction::Jump(JumpISpan { addr: 11 })) .expect_instr(8, Instruction::PushBoolean(true, lc(5, 1))) .expect_instr(9, Instruction::JumpIfNotTrue(11)) - .expect_instr(10, Instruction::BuiltinCall(SymbolKey::from("BAZ"), lc(6, 1), 0)) + .expect_instr( + 10, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("BAZ"), + name_pos: lc(6, 1), + nargs: 0, + }), + ) .check(); } @@ -2550,7 +2625,14 @@ mod tests { n += 1; } t.expect_instr(n, Instruction::JumpIfNotTrue(n + 2)) - .expect_instr(n + 1, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(3, 1), 0)) + .expect_instr( + n + 1, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(3, 1), + nargs: 0, + }), + ) .expect_instr( n + 2, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(4, 1) }), @@ -2694,7 +2776,14 @@ mod tests { .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(7)) - .expect_instr(6, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(3, 1), 0)) + .expect_instr( + 6, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(3, 1), + nargs: 0, + }), + ) .expect_instr( 7, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(4, 1) }), @@ -2728,7 +2817,14 @@ mod tests { .compile() .expect_instr(0, Instruction::PushInteger(5, lc(1, 13))) .expect_instr(1, Instruction::Assign(SymbolKey::from("0select1"))) - .expect_instr(2, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(3, 1), 0)) + .expect_instr( + 2, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(3, 1), + nargs: 0, + }), + ) .expect_instr( 3, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(4, 1) }), @@ -2749,13 +2845,27 @@ mod tests { .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) - .expect_instr(6, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(3, 1), 0)) + .expect_instr( + 6, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(3, 1), + nargs: 0, + }), + ) .expect_instr(7, Instruction::Jump(JumpISpan { addr: 13 })) .expect_instr(8, Instruction::LoadInteger(SymbolKey::from("0select1"), lc(4, 12))) .expect_instr(9, Instruction::PushInteger(8, lc(4, 12))) .expect_instr(10, Instruction::NotEqualIntegers(lc(4, 12))) .expect_instr(11, Instruction::JumpIfNotTrue(13)) - .expect_instr(12, Instruction::BuiltinCall(SymbolKey::from("BAR"), lc(5, 1), 0)) + .expect_instr( + 12, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("BAR"), + name_pos: lc(5, 1), + nargs: 0, + }), + ) .expect_instr( 13, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(6, 1) }), @@ -2776,9 +2886,23 @@ mod tests { .expect_instr(3, Instruction::PushInteger(7, lc(2, 6))) .expect_instr(4, Instruction::EqualIntegers(lc(2, 6))) .expect_instr(5, Instruction::JumpIfNotTrue(8)) - .expect_instr(6, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(3, 1), 0)) + .expect_instr( + 6, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(3, 1), + nargs: 0, + }), + ) .expect_instr(7, Instruction::Jump(JumpISpan { addr: 9 })) - .expect_instr(8, Instruction::BuiltinCall(SymbolKey::from("BAR"), lc(5, 1), 0)) + .expect_instr( + 8, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("BAR"), + name_pos: lc(5, 1), + nargs: 0, + }), + ) .expect_instr( 9, Instruction::Unset(UnsetISpan { name: SymbolKey::from("0select1"), pos: lc(6, 1) }), @@ -2820,7 +2944,14 @@ mod tests { .compile() .expect_instr(0, Instruction::PushBoolean(true, lc(1, 7))) .expect_instr(1, Instruction::JumpIfNotTrue(4)) - .expect_instr(2, Instruction::BuiltinCall(SymbolKey::from("FOO"), lc(2, 1), 0)) + .expect_instr( + 2, + Instruction::BuiltinCall(BuiltinCallISpan { + name: SymbolKey::from("FOO"), + name_pos: lc(2, 1), + nargs: 0, + }), + ) .expect_instr(3, Instruction::Jump(JumpISpan { addr: 0 })) .check(); } diff --git a/core/src/exec.rs b/core/src/exec.rs index ad84e1c6..8545869b 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1270,12 +1270,12 @@ impl Machine { context.pc += 1; } - Instruction::BuiltinCall(name, bref_pos, nargs) => { + Instruction::BuiltinCall(span) => { return Ok(InternalStopReason::Upcall(UpcallData { - name: name.clone(), + name: span.name.clone(), return_type: None, - pos: *bref_pos, - nargs: *nargs, + pos: span.name_pos, + nargs: span.nargs, })); } @@ -1292,12 +1292,12 @@ impl Machine { context.pc += 1; } - Instruction::FunctionCall(name, return_type, pos, nargs) => { + Instruction::FunctionCall(span) => { return Ok(InternalStopReason::Upcall(UpcallData { - name: name.clone(), - return_type: Some(*return_type), - pos: *pos, - nargs: *nargs, + name: span.name.clone(), + return_type: Some(span.return_type), + pos: span.name_pos, + nargs: span.nargs, })); } From 405a3f2e7212c8687a75fdfd7cc047cc6849f931 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 8 Jan 2026 18:44:51 -0800 Subject: [PATCH 071/110] Turn fixups into a proper enum This feels clearer and leaves room for implementing a different type of fixup required for function and subroutine calls. --- core/src/compiler/mod.rs | 66 ++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index eef19206..66183d7b 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -242,39 +242,33 @@ impl SymbolsTable { } } -/// Indicates the type of fixup required at the address. -enum FixupType { - Gosub, - Goto, - OnError, -} - /// Describes a location in the code needs fixing up after all addresses have been laid out. -struct Fixup { - target: String, - target_pos: LineCol, - ftype: FixupType, +#[allow(clippy::enum_variant_names)] +enum Fixup { + CallAddr(String, LineCol), + GotoAddr(String, LineCol), + OnErrorGotoAddr(String, LineCol), } impl Fixup { /// Constructs a `Fixup` for an `EXIT` instruction. fn from_exit(target: String, span: ExitSpan) -> Self { - Self { target, target_pos: span.pos, ftype: FixupType::Goto } + Self::GotoAddr(target, span.pos) } /// Constructs a `Fixup` for a `GOSUB` instruction. fn from_gosub(span: GotoSpan) -> Self { - Self { target: span.target, target_pos: span.target_pos, ftype: FixupType::Gosub } + Self::CallAddr(span.target, span.target_pos) } /// Constructs a `Fixup` for a `GOTO` instruction. fn from_goto(span: GotoSpan) -> Self { - Self { target: span.target, target_pos: span.target_pos, ftype: FixupType::Goto } + Self::GotoAddr(span.target, span.target_pos) } /// Constructs a `Fixup` for a `ON ERROR GOTO` instruction. fn from_on_error(span: GotoSpan) -> Self { - Self { target: span.target, target_pos: span.target_pos, ftype: FixupType::OnError } + Self::OnErrorGotoAddr(span.target, span.target_pos) } } @@ -1150,6 +1144,18 @@ impl Compiler { Ok(()) } + /// Gets the address of a label. + fn get_label_addr( + labels: &HashMap, + label: String, + pos: LineCol, + ) -> Result { + match labels.get(&label) { + Some(addr) => Ok(*addr), + None => Err(Error::UnknownLabel(pos, label.to_owned())), + } + } + /// Finishes compilation and returns the image representing the compiled program. #[allow(clippy::wrong_self_convention)] fn to_image(mut self) -> Result<(Image, SymbolsTable)> { @@ -1158,20 +1164,28 @@ impl Compiler { } for (pc, fixup) in self.fixups { - let addr = match self.labels.get(&fixup.target) { - Some(addr) => *addr, - None => { - return Err(Error::UnknownLabel(fixup.target_pos, fixup.target)); + let new_instr = match fixup { + Fixup::CallAddr(label, pos) => { + let addr = Self::get_label_addr(&self.labels, label, pos)?; + Instruction::Call(JumpISpan { addr }) } - }; - match fixup.ftype { - FixupType::Gosub => self.instrs[pc] = Instruction::Call(JumpISpan { addr }), - FixupType::Goto => self.instrs[pc] = Instruction::Jump(JumpISpan { addr }), - FixupType::OnError => { - self.instrs[pc] = Instruction::SetErrorHandler(ErrorHandlerISpan::Jump(addr)) + Fixup::GotoAddr(label, pos) => { + let addr = Self::get_label_addr(&self.labels, label, pos)?; + Instruction::Jump(JumpISpan { addr }) } - } + + Fixup::OnErrorGotoAddr(label, pos) => { + let addr = Self::get_label_addr(&self.labels, label, pos)?; + Instruction::SetErrorHandler(ErrorHandlerISpan::Jump(addr)) + } + }; + debug_assert_eq!( + std::mem::discriminant(&Instruction::Nop), + std::mem::discriminant(&self.instrs[pc]), + "Fixup target address must contain a Nop instruction", + ); + self.instrs[pc] = new_instr; } let image = Image { instrs: self.instrs, data: self.data }; Ok((image, self.symtable)) From bbc08a1c28b033f8d06ec1b1754c53636d3d67b2 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 8 Jan 2026 21:41:18 -0800 Subject: [PATCH 072/110] Remove next_pc tracking We can do the same by inspecting instrs.len() and not having to track a separate variable that contains the same information. --- core/src/compiler/mod.rs | 57 ++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 66183d7b..107cefcb 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -275,9 +275,6 @@ impl Fixup { /// Compilation context to accumulate the results of the translation of various translation units. #[derive(Default)] struct Compiler { - /// Address of the next instruction to be emitted. - next_pc: Address, - /// Current nesting of `DO` loops, needed to assign targets for `EXIT DO` statements. /// /// The first component of this pair indicates which block of `EXIT DO`s we are dealing with and @@ -323,10 +320,8 @@ struct Compiler { impl Compiler { /// Appends an instruction to the bytecode and returns its address. fn emit(&mut self, instr: Instruction) -> Address { - let pc = self.next_pc; self.instrs.push(instr); - self.next_pc += 1; - pc + self.instrs.len() - 1 } /// Generates a fake label for the epilogue a callable based on its `name`. @@ -392,7 +387,6 @@ impl Compiler { let mut instrs = vec![]; match exprs::compile_array_indices(&mut instrs, &self.symtable, exp_nargs, args, name_pos) { Ok(result) => { - self.next_pc += instrs.len(); self.instrs.append(&mut instrs); Ok(result) } @@ -538,38 +532,38 @@ impl Compiler { let end_pc; match span.guard { DoGuard::Infinite => { - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); } DoGuard::PreUntil(guard) => { - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); self.compile_expr_guard(guard, "DO")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); - self.instrs[jump_pc] = Instruction::JumpIfTrue(self.next_pc); + self.instrs[jump_pc] = Instruction::JumpIfTrue(self.instrs.len()); } DoGuard::PreWhile(guard) => { - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); self.compile_expr_guard(guard, "DO")?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); - self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); + self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.instrs.len()); } DoGuard::PostUntil(guard) => { - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); self.compile_many(span.body)?; self.compile_expr_guard(guard, "LOOP")?; end_pc = self.emit(Instruction::JumpIfNotTrue(start_pc)); } DoGuard::PostWhile(guard) => { - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); self.compile_many(span.body)?; self.compile_expr_guard(guard, "LOOP")?; end_pc = self.emit(Instruction::JumpIfTrue(start_pc)); @@ -613,13 +607,13 @@ impl Compiler { self.instrs[skip_pc] = Instruction::JumpIfDefined(JumpIfDefinedISpan { var: iter_key, - addr: self.next_pc, + addr: self.instrs.len(), }); } self.compile_assignment(span.iter.clone(), span.iter_pos, span.start)?; - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); let end_etype = self.compile_expr(span.end)?; match end_etype { ExprType::Boolean => (), @@ -633,7 +627,7 @@ impl Compiler { let end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); - self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); + self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.instrs.len()); let existing = self .labels @@ -661,17 +655,17 @@ impl Compiler { self.compile_many(branch.body)?; if next2.is_some() { - end_pcs.push(self.next_pc); + end_pcs.push(self.instrs.len()); self.emit(Instruction::Nop); } - self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); + self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.instrs.len()); next = next2; } for end_pc in end_pcs { - self.instrs[end_pc] = Instruction::Jump(JumpISpan { addr: self.next_pc }); + self.instrs[end_pc] = Instruction::Jump(JumpISpan { addr: self.instrs.len() }); } Ok(()) @@ -768,11 +762,11 @@ impl Compiler { self.compile_many(case.body)?; if next2.is_some() { - end_pcs.push(self.next_pc); + end_pcs.push(self.instrs.len()); self.emit(Instruction::Nop); } - self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); + self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.instrs.len()); } } @@ -780,7 +774,7 @@ impl Compiler { } for end_pc in end_pcs { - self.instrs[end_pc] = Instruction::Jump(JumpISpan { addr: self.next_pc }); + self.instrs[end_pc] = Instruction::Jump(JumpISpan { addr: self.instrs.len() }); } let test_key = SymbolKey::from(test_vref.name()); @@ -792,7 +786,7 @@ impl Compiler { /// Compiles a `WHILE` loop and appends its instructions to the compilation context. fn compile_while(&mut self, span: WhileSpan) -> Result<()> { - let start_pc = self.next_pc; + let start_pc = self.instrs.len(); self.compile_expr_guard(span.expr, "WHILE")?; let jump_pc = self.emit(Instruction::Nop); @@ -800,7 +794,7 @@ impl Compiler { self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); - self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.next_pc); + self.instrs[jump_pc] = Instruction::JumpIfNotTrue(self.instrs.len()); Ok(()) } @@ -809,10 +803,7 @@ impl Compiler { /// compilation context, and returns the type of the compiled expression. fn compile_expr(&mut self, expr: Expr) -> Result { match compile_expr(&mut self.instrs, &self.symtable, expr, false) { - Ok(result) => { - self.next_pc = self.instrs.len(); - Ok(result) - } + Ok(result) => Ok(result), Err(e) => Err(e), } } @@ -821,7 +812,6 @@ impl Compiler { /// instructions to the compilation context, and returns the type of the compiled expression. fn compile_expr_as_type(&mut self, expr: Expr, target: ExprType) -> Result<()> { compile_expr_as_type(&mut self.instrs, &self.symtable, expr, target)?; - self.next_pc = self.instrs.len(); Ok(()) } @@ -871,7 +861,6 @@ impl Compiler { name_pos, span.args, )?; - self.next_pc = self.instrs.len(); self.emit(Instruction::BuiltinCall(BuiltinCallISpan { name: key, name_pos: span.vref_pos, @@ -1001,7 +990,7 @@ impl Compiler { } Statement::Label(span) => { - if self.labels.insert(span.name.clone(), self.next_pc).is_some() { + if self.labels.insert(span.name.clone(), self.instrs.len()).is_some() { return Err(Error::DuplicateLabel(span.name_pos, span.name)); } } @@ -1045,7 +1034,7 @@ impl Compiler { let mut subs = HashMap::with_capacity(self.callable_spans.len()); let callable_spans = std::mem::take(&mut self.callable_spans); for span in callable_spans { - let pc = self.next_pc; + let pc = self.instrs.len(); let key = SymbolKey::from(span.name.name()); let return_value = Compiler::return_key(&key); @@ -1139,7 +1128,7 @@ impl Compiler { } } - self.instrs[end] = Instruction::Jump(JumpISpan { addr: self.next_pc }); + self.instrs[end] = Instruction::Jump(JumpISpan { addr: self.instrs.len() }); Ok(()) } From f8e8057c1645d7f801e3694c1ae2c0cbdce6be55 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 8 Jan 2026 18:12:43 -0800 Subject: [PATCH 073/110] Use fixups to call user-defined subs and functions Instead of abusing the instructions to call builtin commands and functions during code generation and then trying to replace them with jumps, do the right thing and emit fixups upfront for all call sites. --- core/src/compiler/args.rs | 36 +++++++--- core/src/compiler/exprs.rs | 142 +++++++++++++++++++++++++++---------- core/src/compiler/mod.rs | 97 ++++++++++++------------- core/src/syms.rs | 13 ++-- 4 files changed, 188 insertions(+), 100 deletions(-) diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 5731f9a4..c621018a 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -18,12 +18,13 @@ use crate::ast::*; use crate::bytecode::*; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; -use crate::compiler::{Error, ExprType, Result, SymbolPrototype, SymbolsTable}; +use crate::compiler::{Error, ExprType, Fixup, Result, SymbolPrototype, SymbolsTable}; use crate::exec::ValueTag; use crate::reader::LineCol; use crate::syms::CallableMetadata; use crate::syms::SymbolKey; use std::borrow::Cow; +use std::collections::HashMap; use std::ops::RangeInclusive; /// Details to compile a required scalar parameter. @@ -489,6 +490,7 @@ fn compile_syn_argsep( fn compile_args( md: &CallableMetadata, instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, pos: LineCol, args: Vec, @@ -525,7 +527,7 @@ fn compile_args( match syn.type_syn { RepeatedTypeSyntax::AnyValue => { debug_assert!(need_tags); - let etype = compile_expr(instrs, symtable, expr, false)?; + let etype = compile_expr(instrs, fixups, symtable, expr, false)?; instrs .push(Instruction::PushInteger(ValueTag::from(etype) as i32, pos)); nargs += 2; @@ -548,7 +550,7 @@ fn compile_args( } RepeatedTypeSyntax::TypedValue(vtype) => { - compile_expr_as_type(instrs, symtable, expr, vtype)?; + compile_expr_as_type(instrs, fixups, symtable, expr, vtype)?; if need_tags { instrs.push(Instruction::PushInteger( ValueTag::from(vtype) as i32, @@ -596,7 +598,7 @@ fn compile_args( SingularArgSyntax::RequiredValue(details, sep) => { match span.expr { Some(expr) => { - compile_expr_as_type(instrs, symtable, expr, details.vtype)?; + compile_expr_as_type(instrs, fixups, symtable, expr, details.vtype)?; nargs += 1; } None => return Err(Error::CallableSyntaxError(pos, md.clone())), @@ -625,7 +627,7 @@ fn compile_args( let (tag, pos) = match span.expr { Some(expr) => { let pos = expr.start_pos(); - compile_expr_as_type(instrs, symtable, expr, details.vtype)?; + compile_expr_as_type(instrs, fixups, symtable, expr, details.vtype)?; nargs += 1; (details.present_value, pos) } @@ -640,7 +642,7 @@ fn compile_args( let (tag, pos) = match span.expr { Some(expr) => { let pos = expr.start_pos(); - let etype = compile_expr(instrs, symtable, expr, false)?; + let etype = compile_expr(instrs, fixups, symtable, expr, false)?; nargs += 2; (ValueTag::from(etype), pos) } @@ -681,11 +683,12 @@ fn compile_args( pub(super) fn compile_command_args( md: &CallableMetadata, instrs: &mut Vec, + fixups: &mut HashMap, symtable: &mut SymbolsTable, pos: LineCol, args: Vec, ) -> Result { - let (nargs, to_insert) = compile_args(md, instrs, symtable, pos, args)?; + let (nargs, to_insert) = compile_args(md, instrs, fixups, symtable, pos, args)?; for (key, proto) in to_insert { if !symtable.contains_key(&key) { symtable.insert(key, proto); @@ -701,11 +704,12 @@ pub(super) fn compile_command_args( pub(super) fn compile_function_args( md: &CallableMetadata, instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, pos: LineCol, args: Vec, ) -> Result { - let (nargs, to_insert) = compile_args(md, instrs, symtable, pos, args)?; + let (nargs, to_insert) = compile_args(md, instrs, fixups, symtable, pos, args)?; debug_assert!(to_insert.is_empty()); Ok(nargs) } @@ -755,15 +759,24 @@ mod testutils { // Start with one instruction to validate that the args compiler doesn't touch it. Instruction::Nop, ]; + let mut fixups = HashMap::default(); let md = CallableMetadataBuilder::new("TEST").with_syntaxes(self.syntaxes).test_build(); - let result = - compile_command_args(&md, &mut instrs, &mut self.symtable, lc(1000, 2000), args); + let result = compile_command_args( + &md, + &mut instrs, + &mut fixups, + &mut self.symtable, + lc(1000, 2000), + args, + ); Checker { result, instrs, + fixups, symtable: self.symtable, exp_result: Ok(0), exp_instrs: vec![Instruction::Nop], + exp_fixups: HashMap::default(), exp_vars: HashMap::default(), } } @@ -774,9 +787,11 @@ mod testutils { pub(super) struct Checker { result: Result, instrs: Vec, + fixups: HashMap, symtable: SymbolsTable, exp_result: Result, exp_instrs: Vec, + exp_fixups: HashMap, exp_vars: HashMap, } @@ -818,6 +833,7 @@ mod testutils { } assert_eq!(self.exp_instrs, self.instrs); + assert_eq!(self.exp_fixups, self.fixups); let mut exp_keys = self.symtable.keys(); for (key, exp_etype) in &self.exp_vars { diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 900be859..ce15d32a 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -15,17 +15,34 @@ //! Functions to convert expressions into bytecode. -use super::{Error, ExprType, Result, SymbolPrototype, SymbolsTable}; +use super::{Error, ExprType, Fixup, Result, SymbolPrototype, SymbolsTable}; use crate::ast::*; use crate::bytecode::*; use crate::compiler::compile_function_args; use crate::parser::argspans_to_exprs; use crate::reader::LineCol; use crate::syms::SymbolKey; +use std::collections::HashMap; + +/// Adjusts `src` fixups by -1 if `adjust` is true else leaves them as is, and then merges the +/// results into `dest`. +fn merge_fixups(dest: &mut HashMap, src: HashMap, adjust: bool) { + let expected_size = dest.len() + src.len(); + if !adjust { + dest.extend(src); + } else { + for (pc, fixup) in src { + let previous = dest.insert(pc - 1, fixup); + debug_assert!(previous.is_none()); + } + } + debug_assert_eq!(dest.len(), expected_size); +} /// Compiles the indices used to address an array. pub(super) fn compile_array_indices( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, exp_nargs: usize, args: Vec, @@ -37,7 +54,7 @@ pub(super) fn compile_array_indices( for arg in args.into_iter().rev() { let arg_pos = arg.start_pos(); - match compile_expr(instrs, symtable, arg, false)? { + match compile_expr(instrs, fixups, symtable, arg, false)? { ExprType::Integer => (), ExprType::Double => { instrs.push(Instruction::DoubleToInteger); @@ -54,10 +71,11 @@ pub(super) fn compile_array_indices( /// Compiles a logical or bitwise unary operator and appends its instructions to `instrs`. fn compile_not_op( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, span: UnaryOpSpan, ) -> Result { - let expr_type = compile_expr(instrs, symtable, span.expr, false)?; + let expr_type = compile_expr(instrs, fixups, symtable, span.expr, false)?; match expr_type { ExprType::Boolean => { instrs.push(Instruction::LogicalNot(span.pos)); @@ -74,10 +92,11 @@ fn compile_not_op( /// Compiles a negate operator and appends its instructions to `instrs`. fn compile_neg_op( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, span: UnaryOpSpan, ) -> Result { - let expr_type = compile_expr(instrs, symtable, span.expr, false)?; + let expr_type = compile_expr(instrs, fixups, symtable, span.expr, false)?; match expr_type { ExprType::Double => { instrs.push(Instruction::NegateDouble(span.pos)); @@ -94,14 +113,15 @@ fn compile_neg_op( /// Compiles a logical binary operator and appends its instructions to `instrs`. fn compile_logical_binary_op Instruction, F2: Fn(LineCol) -> Instruction>( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, logical_make_inst: F1, bitwise_make_inst: F2, span: BinaryOpSpan, op_name: &'static str, ) -> Result { - let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; - let rhs_type = compile_expr(instrs, symtable, span.rhs, false)?; + let lhs_type = compile_expr(instrs, fixups, symtable, span.lhs, false)?; + let rhs_type = compile_expr(instrs, fixups, symtable, span.rhs, false)?; match (lhs_type, rhs_type) { (ExprType::Boolean, ExprType::Boolean) => { instrs.push(logical_make_inst(span.pos)); @@ -124,6 +144,7 @@ fn compile_equality_binary_op< F4: Fn(LineCol) -> Instruction, >( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, boolean_make_inst: F1, double_make_inst: F2, @@ -132,12 +153,13 @@ fn compile_equality_binary_op< span: BinaryOpSpan, op_name: &'static str, ) -> Result { - let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; + let lhs_type = compile_expr(instrs, fixups, symtable, span.lhs, false)?; let pc = instrs.len(); instrs.push(Instruction::Nop); let mut keep_nop = false; - let rhs_type = compile_expr(instrs, symtable, span.rhs, false)?; + let mut extra_fixups = HashMap::default(); + let rhs_type = compile_expr(instrs, &mut extra_fixups, symtable, span.rhs, false)?; let result = match (lhs_type, rhs_type) { (lhs_type, rhs_type) if lhs_type == rhs_type => lhs_type, @@ -161,6 +183,7 @@ fn compile_equality_binary_op< let nop = instrs.remove(pc); debug_assert_eq!(std::mem::discriminant(&Instruction::Nop), std::mem::discriminant(&nop)); } + merge_fixups(fixups, extra_fixups, !keep_nop); match result { ExprType::Boolean => instrs.push(boolean_make_inst(span.pos)), @@ -179,6 +202,7 @@ fn compile_relational_binary_op< F3: Fn(LineCol) -> Instruction, >( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, double_make_inst: F1, integer_make_inst: F2, @@ -186,12 +210,13 @@ fn compile_relational_binary_op< span: BinaryOpSpan, op_name: &'static str, ) -> Result { - let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; + let lhs_type = compile_expr(instrs, fixups, symtable, span.lhs, false)?; let pc = instrs.len(); instrs.push(Instruction::Nop); let mut keep_nop = false; - let rhs_type = compile_expr(instrs, symtable, span.rhs, false)?; + let mut extra_fixups = HashMap::default(); + let rhs_type = compile_expr(instrs, &mut extra_fixups, symtable, span.rhs, false)?; let result = match (lhs_type, rhs_type) { // Boolean is explicitly excluded here. (ExprType::Double, ExprType::Double) => ExprType::Double, @@ -218,6 +243,7 @@ fn compile_relational_binary_op< let nop = instrs.remove(pc); debug_assert_eq!(std::mem::discriminant(&Instruction::Nop), std::mem::discriminant(&nop)); } + merge_fixups(fixups, extra_fixups, !keep_nop); match result { ExprType::Boolean => unreachable!("Filtered out above"), @@ -232,12 +258,13 @@ fn compile_relational_binary_op< /// Compiles a binary shift operator and appends its instructions to `instrs`. fn compile_shift_binary_op Instruction>( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, make_inst: F, span: BinaryOpSpan, op_name: &'static str, ) -> Result { - let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; + let lhs_type = compile_expr(instrs, fixups, symtable, span.lhs, false)?; match lhs_type { ExprType::Integer => (), _ => { @@ -245,7 +272,7 @@ fn compile_shift_binary_op Instruction>( } }; - let rhs_type = compile_expr(instrs, symtable, span.rhs, false)?; + let rhs_type = compile_expr(instrs, fixups, symtable, span.rhs, false)?; match rhs_type { ExprType::Integer => (), _ => { @@ -262,18 +289,20 @@ fn compile_shift_binary_op Instruction>( /// Compiles a binary operator and appends its instructions to `instrs`. fn compile_arithmetic_binary_op Instruction, F2: Fn(LineCol) -> Instruction>( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, double_make_inst: F1, integer_make_inst: F2, span: BinaryOpSpan, op_name: &'static str, ) -> Result { - let lhs_type = compile_expr(instrs, symtable, span.lhs, false)?; + let lhs_type = compile_expr(instrs, fixups, symtable, span.lhs, false)?; let pc = instrs.len(); instrs.push(Instruction::Nop); let mut keep_nop = false; - let rhs_type = compile_expr(instrs, symtable, span.rhs, false)?; + let mut extra_fixups = HashMap::default(); + let rhs_type = compile_expr(instrs, &mut extra_fixups, symtable, span.rhs, false)?; let result = match (lhs_type, rhs_type) { (ExprType::Double, ExprType::Double) => ExprType::Double, (ExprType::Integer, ExprType::Integer) => ExprType::Integer, @@ -299,6 +328,7 @@ fn compile_arithmetic_binary_op Instruction, F2: Fn(LineCol) let nop = instrs.remove(pc); debug_assert_eq!(std::mem::discriminant(&Instruction::Nop), std::mem::discriminant(&nop)); } + merge_fixups(fixups, extra_fixups, !keep_nop); match result { ExprType::Boolean => unreachable!("Filtered out above"), @@ -316,6 +346,7 @@ fn compile_arithmetic_binary_op Instruction, F2: Fn(LineCol) /// Compiles the load of a symbol in the context of an expression. fn compile_expr_symbol( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, span: SymbolSpan, allow_varrefs: bool, @@ -358,17 +389,20 @@ fn compile_expr_symbol( return Err(Error::CallableSyntaxError(span.pos, md.clone())); } - let nargs = compile_function_args(md, instrs, symtable, span.pos, vec![])?; + let nargs = compile_function_args(md, instrs, fixups, symtable, span.pos, vec![])?; debug_assert_eq!(0, nargs, "Argless compiler must have returned zero arguments"); - ( + let instr = if md.is_builtin() { Instruction::FunctionCall(FunctionCallISpan { name: key, name_pos: span.pos, return_type: etype, nargs: 0, - }), - etype, - ) + }) + } else { + fixups.insert(instrs.len(), Fixup::Call(key, span.pos)); + Instruction::Nop + }; + (instr, etype) } }; if !span.vref.accepts(vtype) { @@ -381,6 +415,7 @@ fn compile_expr_symbol( /// Compiles an array access. fn compile_array_ref( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, span: CallSpan, key: SymbolKey, @@ -389,7 +424,7 @@ fn compile_array_ref( ) -> Result { let exprs = argspans_to_exprs(span.args); let nargs = exprs.len(); - compile_array_indices(instrs, symtable, dimensions, exprs, span.vref_pos)?; + compile_array_indices(instrs, fixups, symtable, dimensions, exprs, span.vref_pos)?; if !span.vref.accepts(vtype) { return Err(Error::IncompatibleTypeAnnotationInReference(span.vref_pos, span.vref)); @@ -406,6 +441,7 @@ fn compile_array_ref( /// don't know if the function wants to take the reference or the value. pub(super) fn compile_expr( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, expr: Expr, allow_varrefs: bool, @@ -431,10 +467,11 @@ pub(super) fn compile_expr( Ok(ExprType::Text) } - Expr::Symbol(span) => compile_expr_symbol(instrs, symtable, span, allow_varrefs), + Expr::Symbol(span) => compile_expr_symbol(instrs, fixups, symtable, span, allow_varrefs), Expr::And(span) => compile_logical_binary_op( instrs, + fixups, symtable, Instruction::LogicalAnd, Instruction::BitwiseAnd, @@ -444,6 +481,7 @@ pub(super) fn compile_expr( Expr::Or(span) => compile_logical_binary_op( instrs, + fixups, symtable, Instruction::LogicalOr, Instruction::BitwiseOr, @@ -453,6 +491,7 @@ pub(super) fn compile_expr( Expr::Xor(span) => compile_logical_binary_op( instrs, + fixups, symtable, Instruction::LogicalXor, Instruction::BitwiseXor, @@ -460,18 +499,30 @@ pub(super) fn compile_expr( "XOR", ), - Expr::Not(span) => compile_not_op(instrs, symtable, *span), + Expr::Not(span) => compile_not_op(instrs, fixups, symtable, *span), Expr::ShiftLeft(span) => { - let result = - compile_shift_binary_op(instrs, symtable, Instruction::ShiftLeft, *span, "<<")?; + let result = compile_shift_binary_op( + instrs, + fixups, + symtable, + Instruction::ShiftLeft, + *span, + "<<", + )?; debug_assert_eq!(ExprType::Integer, result); Ok(result) } Expr::ShiftRight(span) => { - let result = - compile_shift_binary_op(instrs, symtable, Instruction::ShiftRight, *span, ">>")?; + let result = compile_shift_binary_op( + instrs, + fixups, + symtable, + Instruction::ShiftRight, + *span, + ">>", + )?; debug_assert_eq!(ExprType::Integer, result); Ok(result) } @@ -479,6 +530,7 @@ pub(super) fn compile_expr( Expr::Equal(span) => { let result = compile_equality_binary_op( instrs, + fixups, symtable, Instruction::EqualBooleans, Instruction::EqualDoubles, @@ -494,6 +546,7 @@ pub(super) fn compile_expr( Expr::NotEqual(span) => { let result = compile_equality_binary_op( instrs, + fixups, symtable, Instruction::NotEqualBooleans, Instruction::NotEqualDoubles, @@ -509,6 +562,7 @@ pub(super) fn compile_expr( Expr::Less(span) => { let result = compile_relational_binary_op( instrs, + fixups, symtable, Instruction::LessDoubles, Instruction::LessIntegers, @@ -523,6 +577,7 @@ pub(super) fn compile_expr( Expr::LessEqual(span) => { let result = compile_relational_binary_op( instrs, + fixups, symtable, Instruction::LessEqualDoubles, Instruction::LessEqualIntegers, @@ -537,6 +592,7 @@ pub(super) fn compile_expr( Expr::Greater(span) => { let result = compile_relational_binary_op( instrs, + fixups, symtable, Instruction::GreaterDoubles, Instruction::GreaterIntegers, @@ -551,6 +607,7 @@ pub(super) fn compile_expr( Expr::GreaterEqual(span) => { let result = compile_relational_binary_op( instrs, + fixups, symtable, Instruction::GreaterEqualDoubles, Instruction::GreaterEqualIntegers, @@ -564,6 +621,7 @@ pub(super) fn compile_expr( Expr::Add(span) => Ok(compile_arithmetic_binary_op( instrs, + fixups, symtable, Instruction::AddDoubles, Instruction::AddIntegers, @@ -573,6 +631,7 @@ pub(super) fn compile_expr( Expr::Subtract(span) => Ok(compile_arithmetic_binary_op( instrs, + fixups, symtable, Instruction::SubtractDoubles, Instruction::SubtractIntegers, @@ -582,6 +641,7 @@ pub(super) fn compile_expr( Expr::Multiply(span) => Ok(compile_arithmetic_binary_op( instrs, + fixups, symtable, Instruction::MultiplyDoubles, Instruction::MultiplyIntegers, @@ -591,6 +651,7 @@ pub(super) fn compile_expr( Expr::Divide(span) => Ok(compile_arithmetic_binary_op( instrs, + fixups, symtable, Instruction::DivideDoubles, Instruction::DivideIntegers, @@ -600,6 +661,7 @@ pub(super) fn compile_expr( Expr::Modulo(span) => Ok(compile_arithmetic_binary_op( instrs, + fixups, symtable, Instruction::ModuloDoubles, Instruction::ModuloIntegers, @@ -609,6 +671,7 @@ pub(super) fn compile_expr( Expr::Power(span) => Ok(compile_arithmetic_binary_op( instrs, + fixups, symtable, Instruction::PowerDoubles, Instruction::PowerIntegers, @@ -616,13 +679,13 @@ pub(super) fn compile_expr( "^", )?), - Expr::Negate(span) => Ok(compile_neg_op(instrs, symtable, *span)?), + Expr::Negate(span) => Ok(compile_neg_op(instrs, fixups, symtable, *span)?), Expr::Call(span) => { let key = SymbolKey::from(span.vref.name()); match symtable.get(&key) { Some(SymbolPrototype::Array(vtype, dims)) => { - compile_array_ref(instrs, symtable, span, key, *vtype, *dims) + compile_array_ref(instrs, fixups, symtable, span, key, *vtype, *dims) } Some(SymbolPrototype::Callable(md)) => { @@ -645,13 +708,19 @@ pub(super) fn compile_expr( } let span_pos = span.vref_pos; - let nargs = compile_function_args(md, instrs, symtable, span_pos, span.args)?; - instrs.push(Instruction::FunctionCall(FunctionCallISpan { - name: key, - name_pos: span_pos, - return_type: vtype, - nargs, - })); + let nargs = + compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; + if md.is_builtin() { + instrs.push(Instruction::FunctionCall(FunctionCallISpan { + name: key, + name_pos: span_pos, + return_type: vtype, + nargs, + })); + } else { + instrs.push(Instruction::Nop); + fixups.insert(instrs.len() - 1, Fixup::Call(key, span_pos)); + } Ok(vtype) } @@ -669,12 +738,13 @@ pub(super) fn compile_expr( /// possible. pub(super) fn compile_expr_as_type( instrs: &mut Vec, + fixups: &mut HashMap, symtable: &SymbolsTable, expr: Expr, target: ExprType, ) -> Result<()> { let epos = expr.start_pos(); - let etype = compile_expr(instrs, symtable, expr, false)?; + let etype = compile_expr(instrs, fixups, symtable, expr, false)?; if etype == ExprType::Double && target.is_numerical() { if target == ExprType::Integer { instrs.push(Instruction::DoubleToInteger); diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 107cefcb..dfce7f12 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -244,8 +244,10 @@ impl SymbolsTable { /// Describes a location in the code needs fixing up after all addresses have been laid out. #[allow(clippy::enum_variant_names)] +#[cfg_attr(test, derive(Debug, PartialEq))] enum Fixup { CallAddr(String, LineCol), + Call(SymbolKey, LineCol), GotoAddr(String, LineCol), OnErrorGotoAddr(String, LineCol), } @@ -385,7 +387,14 @@ impl Compiler { name_pos: LineCol, ) -> Result<()> { let mut instrs = vec![]; - match exprs::compile_array_indices(&mut instrs, &self.symtable, exp_nargs, args, name_pos) { + match exprs::compile_array_indices( + &mut instrs, + &mut self.fixups, + &self.symtable, + exp_nargs, + args, + name_pos, + ) { Ok(result) => { self.instrs.append(&mut instrs); Ok(result) @@ -492,9 +501,7 @@ impl Compiler { } let mut builder = CallableMetadataBuilder::new_dynamic(span.name.name().to_owned()) - .with_dynamic_syntax(vec![(syntax, None)]) - .with_category("User defined") - .with_description("User defined symbol."); + .with_dynamic_syntax(vec![(syntax, None)]); if let Some(ctype) = span.name.ref_type() { builder = builder.with_return_type(ctype); } @@ -802,7 +809,7 @@ impl Compiler { /// Compiles the evaluation of an expression, appends its instructions to the /// compilation context, and returns the type of the compiled expression. fn compile_expr(&mut self, expr: Expr) -> Result { - match compile_expr(&mut self.instrs, &self.symtable, expr, false) { + match compile_expr(&mut self.instrs, &mut self.fixups, &self.symtable, expr, false) { Ok(result) => Ok(result), Err(e) => Err(e), } @@ -811,7 +818,7 @@ impl Compiler { /// Compiles the evaluation of an expression with casts to a target type, appends its /// instructions to the compilation context, and returns the type of the compiled expression. fn compile_expr_as_type(&mut self, expr: Expr, target: ExprType) -> Result<()> { - compile_expr_as_type(&mut self.instrs, &self.symtable, expr, target)?; + compile_expr_as_type(&mut self.instrs, &mut self.fixups, &self.symtable, expr, target)?; Ok(()) } @@ -857,15 +864,21 @@ impl Compiler { let nargs = compile_command_args( &md, &mut self.instrs, + &mut self.fixups, &mut self.symtable, name_pos, span.args, )?; - self.emit(Instruction::BuiltinCall(BuiltinCallISpan { - name: key, - name_pos: span.vref_pos, - nargs, - })); + if md.is_builtin() { + self.emit(Instruction::BuiltinCall(BuiltinCallISpan { + name: key, + name_pos: span.vref_pos, + nargs, + })); + } else { + let call_pc = self.emit(Instruction::Nop); + self.fixups.insert(call_pc, Fixup::Call(key, span.vref_pos)); + } } Statement::Callable(span) => { @@ -1027,11 +1040,16 @@ impl Compiler { /// Compiles all callables discovered during the first phase and fixes up all call sites to /// point to the compiled code. - fn compile_callables(&mut self) -> Result<()> { + /// + /// Returns a mapping of function and subroutine names to start addresses. + fn compile_callables(&mut self) -> Result> { + if self.callable_spans.is_empty() { + return Ok(HashMap::default()); + } + let end = self.emit(Instruction::Nop); - let mut functions = HashMap::with_capacity(self.callable_spans.len()); - let mut subs = HashMap::with_capacity(self.callable_spans.len()); + let mut callables = HashMap::with_capacity(self.callable_spans.len()); let callable_spans = std::mem::take(&mut self.callable_spans); for span in callable_spans { let pc = self.instrs.len(); @@ -1079,7 +1097,7 @@ impl Compiler { self.labels.insert(Compiler::exit_label_for_callable(&key), epilogue_pc); assert!(existing.is_none(), "Auto-generated label must be unique"); - functions.insert(key, pc); + callables.insert(key, pc); } None => { self.emit(Instruction::EnterScope); @@ -1105,32 +1123,14 @@ impl Compiler { self.labels.insert(Compiler::exit_label_for_callable(&key), epilogue_pc); assert!(existing.is_none(), "Auto-generated label must be unique"); - subs.insert(key, pc); - } - } - } - - for instr in &mut self.instrs { - match instr { - Instruction::BuiltinCall(span) => { - if let Some(addr) = subs.get(&span.name) { - *instr = Instruction::Call(JumpISpan { addr: *addr }); - } - } - - Instruction::FunctionCall(span) => { - if let Some(addr) = functions.get(&span.name) { - *instr = Instruction::Call(JumpISpan { addr: *addr }); - } + callables.insert(key, pc); } - - _ => (), } } self.instrs[end] = Instruction::Jump(JumpISpan { addr: self.instrs.len() }); - Ok(()) + Ok(callables) } /// Gets the address of a label. @@ -1148,9 +1148,7 @@ impl Compiler { /// Finishes compilation and returns the image representing the compiled program. #[allow(clippy::wrong_self_convention)] fn to_image(mut self) -> Result<(Image, SymbolsTable)> { - if !self.callable_spans.is_empty() { - self.compile_callables()?; - } + let callables = self.compile_callables()?; for (pc, fixup) in self.fixups { let new_instr = match fixup { @@ -1159,6 +1157,13 @@ impl Compiler { Instruction::Call(JumpISpan { addr }) } + Fixup::Call(name, pos) => { + let Some(addr) = callables.get(&name) else { + return Err(Error::UndefinedSymbol(pos, name)); + }; + Instruction::Call(JumpISpan { addr: *addr }) + } + Fixup::GotoAddr(label, pos) => { let addr = Self::get_label_addr(&self.labels, label, pos)?; Instruction::Jump(JumpISpan { addr }) @@ -2276,10 +2281,8 @@ mod tests { .expect_symtable( SymbolKey::from("foo"), SymbolPrototype::Callable( - CallableMetadataBuilder::new("USER DEFINED FUNCTION") + CallableMetadataBuilder::new_dynamic("USER DEFINED FUNCTION") .with_syntax(&[(&[], None)]) - .with_category("User defined") - .with_description("User defined function") .build(), ), ) @@ -2311,10 +2314,8 @@ mod tests { .expect_symtable( SymbolKey::from("foo"), SymbolPrototype::Callable( - CallableMetadataBuilder::new("USER DEFINED FUNCTION") + CallableMetadataBuilder::new_dynamic("USER DEFINED FUNCTION") .with_syntax(&[(&[], None)]) - .with_category("User defined") - .with_description("User defined function") .build(), ), ) @@ -2347,10 +2348,8 @@ mod tests { .expect_symtable( SymbolKey::from("foo"), SymbolPrototype::Callable( - CallableMetadataBuilder::new("USER DEFINED FUNCTION") + CallableMetadataBuilder::new_dynamic("USER DEFINED FUNCTION") .with_syntax(&[(&[], None)]) - .with_category("User defined") - .with_description("User defined function") .build(), ), ) @@ -2419,10 +2418,8 @@ mod tests { .expect_symtable( SymbolKey::from("foo"), SymbolPrototype::Callable( - CallableMetadataBuilder::new("USER DEFINED SUB") + CallableMetadataBuilder::new_dynamic("USER DEFINED SUB") .with_syntax(&[(&[], None)]) - .with_category("User defined") - .with_description("User defined sub") .build(), ), ) diff --git a/core/src/syms.rs b/core/src/syms.rs index f24543b5..fe2ba2f0 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -512,13 +512,13 @@ impl CallableMetadataBuilder { /// /// This is the same as `new` but using a dynamically-allocated name, which is necessary for /// user-defined symbols. - pub fn new_dynamic(name: String) -> Self { + pub fn new_dynamic>(name: S) -> Self { Self { - name: Cow::Owned(name.to_ascii_uppercase()), + name: Cow::Owned(name.into().to_ascii_uppercase()), return_type: None, syntaxes: vec![], - category: None, - description: None, + category: Some("User defined"), + description: Some("User defined symbol."), } } @@ -672,6 +672,11 @@ impl CallableMetadata { self.syntaxes.is_empty() || (self.syntaxes.len() == 1 && self.syntaxes[0].is_empty()) } + /// Returns true if this callable is a builtin requiring an upcall during execution. + pub fn is_builtin(&self) -> bool { + self.category != "User defined" + } + /// Returns true if this callable is a function (not a command). pub fn is_function(&self) -> bool { self.return_type.is_some() From 971ff1d1c762ed903168405fcc70892a75fa1045 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 9 Jan 2026 04:19:33 -0800 Subject: [PATCH 074/110] Distinguish builtin callables in the symbols table Instead of adding an is_builtin() method to CallableMetadata, track this information as a different SymbolPrototype in the symbols table. This makes room for tracking other compile-time only information for them. --- core/src/compiler/args.rs | 3 +- core/src/compiler/exprs.rs | 73 ++++++++++++++++++++++++++++---------- core/src/compiler/mod.rs | 21 +++++++---- core/src/syms.rs | 5 --- 4 files changed, 72 insertions(+), 30 deletions(-) diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index c621018a..d5dc5f1e 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -393,7 +393,8 @@ fn compile_required_ref( Ok(None) } - Some(SymbolPrototype::Callable(md)) => { + Some(SymbolPrototype::BuiltinCallable(md)) + | Some(SymbolPrototype::Callable(md)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( span.pos, span.vref, diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index ce15d32a..03f6cb1c 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -196,6 +196,7 @@ fn compile_equality_binary_op< } /// Compiles a relational binary operator and appends its instructions to `instrs`. +#[allow(clippy::too_many_arguments)] fn compile_relational_binary_op< F1: Fn(LineCol) -> Instruction, F2: Fn(LineCol) -> Instruction, @@ -377,7 +378,7 @@ fn compile_expr_symbol( } } - Some(SymbolPrototype::Callable(md)) => { + Some(SymbolPrototype::BuiltinCallable(md)) => { let etype = match md.return_type() { Some(etype) => etype, None => { @@ -391,18 +392,33 @@ fn compile_expr_symbol( let nargs = compile_function_args(md, instrs, fixups, symtable, span.pos, vec![])?; debug_assert_eq!(0, nargs, "Argless compiler must have returned zero arguments"); - let instr = if md.is_builtin() { + ( Instruction::FunctionCall(FunctionCallISpan { name: key, name_pos: span.pos, return_type: etype, nargs: 0, - }) - } else { - fixups.insert(instrs.len(), Fixup::Call(key, span.pos)); - Instruction::Nop + }), + etype, + ) + } + + Some(SymbolPrototype::Callable(md)) => { + let etype = match md.return_type() { + Some(etype) => etype, + None => { + return Err(Error::NotArrayOrFunction(span.pos, key)); + } }; - (instr, etype) + + if !md.is_argless() { + return Err(Error::CallableSyntaxError(span.pos, md.clone())); + } + + let nargs = compile_function_args(md, instrs, fixups, symtable, span.pos, vec![])?; + debug_assert_eq!(0, nargs, "Argless compiler must have returned zero arguments"); + fixups.insert(instrs.len(), Fixup::Call(key, span.pos)); + (Instruction::Nop, etype) } }; if !span.vref.accepts(vtype) { @@ -688,7 +704,7 @@ pub(super) fn compile_expr( compile_array_ref(instrs, fixups, symtable, span, key, *vtype, *dims) } - Some(SymbolPrototype::Callable(md)) => { + Some(SymbolPrototype::BuiltinCallable(md)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( span.vref_pos, @@ -710,17 +726,38 @@ pub(super) fn compile_expr( let span_pos = span.vref_pos; let nargs = compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; - if md.is_builtin() { - instrs.push(Instruction::FunctionCall(FunctionCallISpan { - name: key, - name_pos: span_pos, - return_type: vtype, - nargs, - })); - } else { - instrs.push(Instruction::Nop); - fixups.insert(instrs.len() - 1, Fixup::Call(key, span_pos)); + instrs.push(Instruction::FunctionCall(FunctionCallISpan { + name: key, + name_pos: span_pos, + return_type: vtype, + nargs, + })); + Ok(vtype) + } + + Some(SymbolPrototype::Callable(md)) => { + if !span.vref.accepts_callable(md.return_type()) { + return Err(Error::IncompatibleTypeAnnotationInReference( + span.vref_pos, + span.vref, + )); } + + let vtype = match md.return_type() { + Some(vtype) => vtype, + None => { + return Err(Error::NotArrayOrFunction(span.vref_pos, key)); + } + }; + + if md.is_argless() { + return Err(Error::CallableSyntaxError(span.vref_pos, md.clone())); + } + + let span_pos = span.vref_pos; + compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; + instrs.push(Instruction::Nop); + fixups.insert(instrs.len() - 1, Fixup::Call(key, span_pos)); Ok(vtype) } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index dfce7f12..e5f6a8c3 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -117,6 +117,9 @@ enum SymbolPrototype { /// Information about an array. The integer indicates the number of dimensions in the array. Array(ExprType, usize), + /// Information about a callable that's a builtin and requires an upcall to execute. + BuiltinCallable(CallableMetadata), + /// Information about a callable. Callable(CallableMetadata), @@ -161,7 +164,7 @@ impl From<&Symbols> for SymbolsTable { fn from(syms: &Symbols) -> Self { let mut globals = HashMap::default(); for (name, callable) in syms.callables() { - let proto = SymbolPrototype::Callable(callable.metadata().clone()); + let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone()); globals.insert(name.clone(), proto); } @@ -243,7 +246,6 @@ impl SymbolsTable { } /// Describes a location in the code needs fixing up after all addresses have been laid out. -#[allow(clippy::enum_variant_names)] #[cfg_attr(test, derive(Debug, PartialEq))] enum Fixup { CallAddr(String, LineCol), @@ -845,12 +847,19 @@ impl Compiler { Statement::Call(span) => { let key = SymbolKey::from(&span.vref.name()); - let md = match self.symtable.get(&key) { + let (md, is_builtin) = match self.symtable.get(&key) { + Some(SymbolPrototype::BuiltinCallable(md)) => { + if md.is_function() { + return Err(Error::NotACommand(span.vref_pos, span.vref)); + } + (md.clone(), true) + } + Some(SymbolPrototype::Callable(md)) => { if md.is_function() { return Err(Error::NotACommand(span.vref_pos, span.vref)); } - md.clone() + (md.clone(), false) } Some(_) => { @@ -869,7 +878,7 @@ impl Compiler { name_pos, span.args, )?; - if md.is_builtin() { + if is_builtin { self.emit(Instruction::BuiltinCall(BuiltinCallISpan { name: key, name_pos: span.vref_pos, @@ -1238,7 +1247,7 @@ mod testutils { pub(crate) fn define_callable(mut self, builder: CallableMetadataBuilder) -> Self { let md = builder.test_build(); let key = SymbolKey::from(md.name()); - self.symtable.insert(key, SymbolPrototype::Callable(md)); + self.symtable.insert(key, SymbolPrototype::BuiltinCallable(md)); self } diff --git a/core/src/syms.rs b/core/src/syms.rs index fe2ba2f0..b3c436ab 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -672,11 +672,6 @@ impl CallableMetadata { self.syntaxes.is_empty() || (self.syntaxes.len() == 1 && self.syntaxes[0].is_empty()) } - /// Returns true if this callable is a builtin requiring an upcall during execution. - pub fn is_builtin(&self) -> bool { - self.category != "User defined" - } - /// Returns true if this callable is a function (not a command). pub fn is_function(&self) -> bool { self.return_type.is_some() From d4f5803b79e37f15c9e22768c87c243c39f0427c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 9 Jan 2026 04:43:12 -0800 Subject: [PATCH 075/110] Assign upcall indexes to builtin calls These will later be used during execution to index builtin calls via an array instead of via a hashmap lookup. --- cli/tests/examples/fibonacci.out | 4 +- cli/tests/examples/gpio.out | 92 +++++----- cli/tests/examples/guess.out | 68 ++++---- cli/tests/examples/hello.out | 4 +- cli/tests/examples/palette.out | 24 +-- cli/tests/examples/tour.out | 282 +++++++++++++++---------------- core/src/bytecode.rs | 10 +- core/src/compiler/args.rs | 2 +- core/src/compiler/exprs.rs | 9 +- core/src/compiler/mod.rs | 73 ++++++-- 10 files changed, 314 insertions(+), 254 deletions(-) diff --git a/cli/tests/examples/fibonacci.out b/cli/tests/examples/fibonacci.out index e1d42731..3af465b3 100644 --- a/cli/tests/examples/fibonacci.out +++ b/cli/tests/examples/fibonacci.out @@ -7,7 +7,7 @@ took 177 steps to calculate 0004 PUSH% 1 # 32:28 0005 PUSH$ "fibonacci of 10 is:" # 32:7 0006 PUSH% 4 # 32:7 -0007 CALLB PRINT, 5 # 32:1 +0007 CALLB 48 (PRINT), 5 # 32:1 0008 PUSH$ "steps to calculate" # 33:22 0009 PUSH% 4 # 33:22 000a PUSH% 1 # 33:20 @@ -16,7 +16,7 @@ took 177 steps to calculate 000d PUSH% 1 # 33:13 000e PUSH$ "took" # 33:7 000f PUSH% 4 # 33:7 -0010 CALLB PRINT, 8 # 33:1 +0010 CALLB 48 (PRINT), 8 # 33:1 0011 JMP 0037 0012 ENTER 0013 DIMV% 0RETURN_FIBONACCI diff --git a/cli/tests/examples/gpio.out b/cli/tests/examples/gpio.out index 70b3af82..eb91622f 100644 --- a/cli/tests/examples/gpio.out +++ b/cli/tests/examples/gpio.out @@ -50,32 +50,32 @@ __GPIO_MOCK_DATA 15 1820 0001 SETV BUTTON 0002 PUSH% 18 # 17:7 0003 SETV LED -0004 CALLB CLS, 0 # 19:1 +0004 CALLB 6 (CLS), 0 # 19:1 0005 PUSH% 11 # 20:7 -0006 CALLB COLOR, 1 # 20:1 -0007 CALLB PRINT, 0 # 21:1 +0006 CALLB 7 (COLOR), 1 # 20:1 +0007 CALLB 48 (PRINT), 0 # 21:1 0008 PUSH$ " GPIO demo" # 22:7 0009 PUSH% 4 # 22:7 -000a CALLB PRINT, 2 # 22:1 +000a CALLB 48 (PRINT), 2 # 22:1 000b PUSH$ "===========" # 23:7 000c PUSH% 4 # 23:7 -000d CALLB PRINT, 2 # 23:1 -000e CALLB COLOR, 0 # 24:1 -000f CALLB PRINT, 0 # 25:1 +000d CALLB 48 (PRINT), 2 # 23:1 +000e CALLB 7 (COLOR), 0 # 24:1 +000f CALLB 48 (PRINT), 0 # 25:1 0010 PUSH$ "This demo showcases how to poll a hardware button attached to a GPIO" # 26:7 0011 PUSH% 4 # 26:7 -0012 CALLB PRINT, 2 # 26:1 +0012 CALLB 48 (PRINT), 2 # 26:1 0013 PUSH$ "pin and how to flash an LED attached to another one." # 27:7 0014 PUSH% 4 # 27:7 -0015 CALLB PRINT, 2 # 27:1 -0016 CALLB PRINT, 0 # 28:1 +0015 CALLB 48 (PRINT), 2 # 27:1 +0016 CALLB 48 (PRINT), 0 # 28:1 0017 PUSH% 2 # 29:7 -0018 CALLB COLOR, 1 # 29:1 +0018 CALLB 7 (COLOR), 1 # 29:1 0019 PUSH$ "To get started, follow these steps:" # 30:7 001a PUSH% 4 # 30:7 -001b CALLB PRINT, 2 # 30:1 -001c CALLB COLOR, 0 # 31:1 -001d CALLB PRINT, 0 # 32:1 +001b CALLB 48 (PRINT), 2 # 30:1 +001c CALLB 7 (COLOR), 0 # 31:1 +001d CALLB 48 (PRINT), 0 # 32:1 001e PUSH$ "and to ground; don't forget to add a" # 33:40 001f PUSH% 4 # 33:40 0020 PUSH% 1 # 33:38 @@ -84,11 +84,11 @@ __GPIO_MOCK_DATA 15 1820 0023 PUSH% 1 # 33:33 0024 PUSH$ "1. Connect an LED to pin" # 33:7 0025 PUSH% 4 # 33:7 -0026 CALLB PRINT, 8 # 33:1 +0026 CALLB 48 (PRINT), 8 # 33:1 0027 PUSH$ " resistor inline." # 34:7 0028 PUSH% 4 # 34:7 -0029 CALLB PRINT, 2 # 34:1 -002a CALLB PRINT, 0 # 35:1 +0029 CALLB 48 (PRINT), 2 # 34:1 +002a CALLB 48 (PRINT), 0 # 35:1 002b PUSH$ "and to ground. We'll be" # 36:50 002c PUSH% 4 # 36:50 002d PUSH% 1 # 36:48 @@ -97,50 +97,50 @@ __GPIO_MOCK_DATA 15 1820 0030 PUSH% 1 # 36:40 0031 PUSH$ "2. Connect a push button to pin" # 36:7 0032 PUSH% 4 # 36:7 -0033 CALLB PRINT, 8 # 36:1 +0033 CALLB 48 (PRINT), 8 # 36:1 0034 PUSH$ " using the built-in pull-up resistor for the input pin so there is" # 37:7 0035 PUSH% 4 # 37:7 -0036 CALLB PRINT, 2 # 37:1 +0036 CALLB 48 (PRINT), 2 # 37:1 0037 PUSH$ " no need to do any extra wiring." # 38:7 0038 PUSH% 4 # 38:7 -0039 CALLB PRINT, 2 # 38:1 -003a CALLB PRINT, 0 # 39:1 +0039 CALLB 48 (PRINT), 2 # 38:1 +003a CALLB 48 (PRINT), 0 # 39:1 003b PUSH% 1 # 40:7 -003c CALLB COLOR, 1 # 40:1 +003c CALLB 7 (COLOR), 1 # 40:1 003d PUSH$ "This demo is only functional on the Raspberry Pi and assumes you have" # 41:7 003e PUSH% 4 # 41:7 -003f CALLB PRINT, 2 # 41:1 +003f CALLB 48 (PRINT), 2 # 41:1 0040 PUSH$ "built EndBASIC with --features=rpi. If these conditions are not met," # 42:7 0041 PUSH% 4 # 42:7 -0042 CALLB PRINT, 2 # 42:1 +0042 CALLB 48 (PRINT), 2 # 42:1 0043 PUSH$ "the demo will fail to run." # 43:7 0044 PUSH% 4 # 43:7 -0045 CALLB PRINT, 2 # 43:1 -0046 CALLB COLOR, 0 # 44:1 -0047 CALLB PRINT, 0 # 45:1 +0045 CALLB 48 (PRINT), 2 # 43:1 +0046 CALLB 7 (COLOR), 0 # 44:1 +0047 CALLB 48 (PRINT), 0 # 45:1 0048 LOADR DUMMY # 46:71 0049 PUSH% 2 # 46:69 004a PUSH$ "Press ENTER when you are ready or CTRL+C to exit the demo..." # 46:7 004b PUSH% 1 # 46:7 -004c CALLB INPUT, 4 # 46:1 -004d CALLB CLS, 0 # 48:1 +004c CALLB 30 (INPUT), 4 # 46:1 +004d CALLB 6 (CLS), 0 # 48:1 004e PUSH% 11 # 49:7 -004f CALLB COLOR, 1 # 49:1 -0050 CALLB PRINT, 0 # 50:1 +004f CALLB 7 (COLOR), 1 # 49:1 +0050 CALLB 48 (PRINT), 0 # 50:1 0051 PUSH$ " GPIO demo" # 51:7 0052 PUSH% 4 # 51:7 -0053 CALLB PRINT, 2 # 51:1 +0053 CALLB 48 (PRINT), 2 # 51:1 0054 PUSH$ "===========" # 52:7 0055 PUSH% 4 # 52:7 -0056 CALLB PRINT, 2 # 52:1 -0057 CALLB COLOR, 0 # 53:1 -0058 CALLB PRINT, 0 # 54:1 +0056 CALLB 48 (PRINT), 2 # 52:1 +0057 CALLB 7 (COLOR), 0 # 53:1 +0058 CALLB 48 (PRINT), 0 # 54:1 0059 PUSH$ "IN-PULL-UP" # 57:20 005a LOAD% BUTTON # 57:12 -005b CALLB GPIO_SETUP, 2 # 57:1 +005b CALLB 26 (GPIO_SETUP), 2 # 57:1 005c PUSH$ "OUT" # 58:17 005d LOAD% LED # 58:12 -005e CALLB GPIO_SETUP, 2 # 58:1 +005e CALLB 26 (GPIO_SETUP), 2 # 58:1 005f PUSH$ "..." # 62:52 0060 PUSH% 4 # 62:52 0061 PUSH% 1 # 62:50 @@ -149,12 +149,12 @@ __GPIO_MOCK_DATA 15 1820 0064 PUSH% 1 # 62:42 0065 PUSH$ "Waiting for a button press on pin" # 62:7 0066 PUSH% 4 # 62:7 -0067 CALLB PRINT, 8 # 62:1 +0067 CALLB 48 (PRINT), 8 # 62:1 0068 LOAD% BUTTON # 63:17 -0069 CALLF? GPIO_READ, 1 # 63:7 +0069 CALLF? 25 (GPIO_READ), 1 # 63:7 006a JMPNT 006e 006b PUSH# 0.05 # 64:11 -006c CALLB SLEEP, 1 # 64:5 +006c CALLB 64 (SLEEP), 1 # 64:5 006d JMP 0068 006e PUSH$ "..." # 68:51 006f PUSH% 4 # 68:51 @@ -164,7 +164,7 @@ __GPIO_MOCK_DATA 15 1820 0073 PUSH% 1 # 68:44 0074 PUSH$ "Button pressed! Blinking LED on pin" # 68:7 0075 PUSH% 4 # 68:7 -0076 CALLB PRINT, 8 # 68:1 +0076 CALLB 48 (PRINT), 8 # 68:1 0077 PUSH% 5 # 69:9 0078 SETV I 0079 LOAD% I # 69:5 @@ -173,17 +173,17 @@ __GPIO_MOCK_DATA 15 1820 007c JMPNT 008f 007d PUSH? true # 70:21 007e LOAD% LED # 70:16 -007f CALLB GPIO_WRITE, 2 # 70:5 +007f CALLB 27 (GPIO_WRITE), 2 # 70:5 0080 PUSH# 0.1 # 71:11 -0081 CALLB SLEEP, 1 # 71:5 +0081 CALLB 64 (SLEEP), 1 # 71:5 0082 PUSH? false # 72:21 0083 LOAD% LED # 72:16 -0084 CALLB GPIO_WRITE, 2 # 72:5 +0084 CALLB 27 (GPIO_WRITE), 2 # 72:5 0085 PUSH# 0.1 # 73:11 -0086 CALLB SLEEP, 1 # 73:5 +0086 CALLB 64 (SLEEP), 1 # 73:5 0087 LOAD% I # 74:11 0088 PUSH% 3 # 74:11 -0089 CALLB PRINT, 2 # 74:5 +0089 CALLB 48 (PRINT), 2 # 74:5 008a LOAD% I # 69:5 008b PUSH% -1 # 69:22 008c ADD% # 69:11 diff --git a/cli/tests/examples/guess.out b/cli/tests/examples/guess.out index 3d07f840..afc36a90 100644 --- a/cli/tests/examples/guess.out +++ b/cli/tests/examples/guess.out @@ -55,36 +55,36 @@ Thanks for playing 000b PUSH% 1 # 22:12 000c LOAD% FG # 22:7 000d PUSH% 1 # 22:7 -000e CALLB COLOR, 4 # 22:1 -000f CALLB CLS, 0 # 23:1 -0010 CALLB PRINT, 0 # 24:1 +000e CALLB 7 (COLOR), 4 # 22:1 +000f CALLB 6 (CLS), 0 # 23:1 +0010 CALLB 48 (PRINT), 0 # 24:1 0011 LOAD% BG # 25:15 0012 PUSH% 1 # 25:15 0013 LOAD% TITLE # 25:7 0014 PUSH% 1 # 25:7 -0015 CALLB COLOR, 4 # 25:1 +0015 CALLB 7 (COLOR), 4 # 25:1 0016 PUSH$ " Guess the number!" # 26:7 0017 PUSH% 4 # 26:7 -0018 CALLB PRINT, 2 # 26:1 +0018 CALLB 48 (PRINT), 2 # 26:1 0019 PUSH$ "===================" # 27:7 001a PUSH% 4 # 27:7 -001b CALLB PRINT, 2 # 27:1 +001b CALLB 48 (PRINT), 2 # 27:1 001c LOAD% BG # 28:12 001d PUSH% 1 # 28:12 001e LOAD% FG # 28:7 001f PUSH% 1 # 28:7 -0020 CALLB COLOR, 4 # 28:1 -0021 CALLB PRINT, 0 # 29:1 +0020 CALLB 7 (COLOR), 4 # 28:1 +0021 CALLB 48 (PRINT), 0 # 29:1 0022 LOADR MAX_NUM # 30:46 0023 PUSH% 1 # 30:44 0024 PUSH$ "What's the largest number I can use" # 30:7 0025 PUSH% 1 # 30:7 -0026 CALLB INPUT, 4 # 30:1 +0026 CALLB 30 (INPUT), 4 # 30:1 0027 LOADR MAX_ATTEMPTS # 31:38 0028 PUSH% 1 # 31:36 0029 PUSH$ "How many attempts each time" # 31:7 002a PUSH% 1 # 31:7 -002b CALLB INPUT, 4 # 31:1 +002b CALLB 30 (INPUT), 4 # 31:1 002c PUSH% 0 # 33:9 002d SETV WINS 002e PUSH% 0 # 34:11 @@ -93,20 +93,20 @@ Thanks for playing 0031 SETV AGAIN 0032 LOAD? AGAIN # 37:7 0033 JMPNT 00b8 -0034 CALLB PRINT, 0 # 38:5 +0034 CALLB 48 (PRINT), 0 # 38:5 0035 PUSH% 1 # 39:25 -0036 CALLF# RND, 1 # 39:20 +0036 CALLF# 55 (RND), 1 # 39:20 0037 LOAD% MAX_NUM # 39:30 0038 %TO# 0039 MUL# # 39:28 -003a CALLF% INT, 1 # 39:15 +003a CALLF% 31 (INT), 1 # 39:15 003b SETV SECRET 003c LOAD% MAX_NUM # 40:60 003d PUSH% 3 # 40:60 003e PUSH% 1 # 40:58 003f PUSH$ "Alright! I have a secret number between 0 and" # 40:11 0040 PUSH% 4 # 40:11 -0041 CALLB PRINT, 5 # 40:5 +0041 CALLB 48 (PRINT), 5 # 40:5 0042 LOAD% MAX_ATTEMPTS # 42:17 0043 SETV ATTEMPTS 0044 PUSH% 1 # 43:15 @@ -120,7 +120,7 @@ Thanks for playing 004c CMPNE% # 44:36 004d AND? # 44:25 004e JMPNT 008a -004f CALLB PRINT, 0 # 45:9 +004f CALLB 48 (PRINT), 0 # 45:9 0050 PUSH$ "attempts left to guess my number" # 46:38 0051 PUSH% 4 # 46:38 0052 PUSH% 1 # 46:36 @@ -129,12 +129,12 @@ Thanks for playing 0055 PUSH% 1 # 46:25 0056 PUSH$ "You have" # 46:15 0057 PUSH% 4 # 46:15 -0058 CALLB PRINT, 8 # 46:9 +0058 CALLB 48 (PRINT), 8 # 46:9 0059 LOADR GUESS # 47:36 005a PUSH% 1 # 47:34 005b PUSH$ "What's your guess" # 47:15 005c PUSH% 1 # 47:15 -005d CALLB INPUT, 4 # 47:9 +005d CALLB 30 (INPUT), 4 # 47:9 005e LOAD% GUESS # 48:12 005f LOAD% SECRET # 48:22 0060 CMPNE% # 48:19 @@ -143,7 +143,7 @@ Thanks for playing 0063 PUSH% 1 # 49:25 0064 LOAD% BAD # 49:19 0065 PUSH% 1 # 49:19 -0066 CALLB COLOR, 4 # 49:13 +0066 CALLB 7 (COLOR), 4 # 49:13 0067 LOAD% GUESS # 50:16 0068 LOAD% SECRET # 50:25 0069 CMPL% # 50:23 @@ -156,7 +156,7 @@ Thanks for playing 0070 PUSH% 1 # 51:31 0071 PUSH$ "Wrong." # 51:23 0072 PUSH% 4 # 51:23 -0073 CALLB PRINT, 8 # 51:17 +0073 CALLB 48 (PRINT), 8 # 51:17 0074 JMP 0080 0075 PUSH? true # 52:13 0076 JMPNT 0080 @@ -168,12 +168,12 @@ Thanks for playing 007c PUSH% 1 # 53:31 007d PUSH$ "Wrong." # 53:23 007e PUSH% 4 # 53:23 -007f CALLB PRINT, 8 # 53:17 +007f CALLB 48 (PRINT), 8 # 53:17 0080 LOAD% BG # 55:24 0081 PUSH% 1 # 55:24 0082 LOAD% FG # 55:19 0083 PUSH% 1 # 55:19 -0084 CALLB COLOR, 4 # 55:13 +0084 CALLB 7 (COLOR), 4 # 55:13 0085 LOAD% ATTEMPTS # 57:21 0086 PUSH% 1 # 57:33 0087 SUB% # 57:31 @@ -191,10 +191,10 @@ Thanks for playing 0093 PUSH% 1 # 62:22 0094 LOAD% GOOD # 62:15 0095 PUSH% 1 # 62:15 -0096 CALLB COLOR, 4 # 62:9 +0096 CALLB 7 (COLOR), 4 # 62:9 0097 PUSH$ "Correct. You win! :-)" # 63:15 0098 PUSH% 4 # 63:15 -0099 CALLB PRINT, 2 # 63:9 +0099 CALLB 48 (PRINT), 2 # 63:9 009a JMP 00ac 009b PUSH? true # 64:5 009c JMPNT 00ac @@ -206,27 +206,27 @@ Thanks for playing 00a2 PUSH% 1 # 66:21 00a3 LOAD% BAD # 66:15 00a4 PUSH% 1 # 66:15 -00a5 CALLB COLOR, 4 # 66:9 +00a5 CALLB 7 (COLOR), 4 # 66:9 00a6 LOAD% SECRET # 67:60 00a7 PUSH% 3 # 67:60 00a8 PUSH% 1 # 67:58 00a9 PUSH$ "Sorry. You lost :-( The secret number was" # 67:15 00aa PUSH% 4 # 67:15 -00ab CALLB PRINT, 5 # 67:9 +00ab CALLB 48 (PRINT), 5 # 67:9 00ac LOAD% BG # 69:16 00ad PUSH% 1 # 69:16 00ae LOAD% FG # 69:11 00af PUSH% 1 # 69:11 -00b0 CALLB COLOR, 4 # 69:5 -00b1 CALLB PRINT, 0 # 70:5 +00b0 CALLB 7 (COLOR), 4 # 69:5 +00b1 CALLB 48 (PRINT), 0 # 70:5 00b2 LOADR AGAIN # 72:40 00b3 PUSH% 1 # 72:38 00b4 PUSH$ "Do you want to play again" # 72:11 00b5 PUSH% 1 # 72:11 -00b6 CALLB INPUT, 4 # 72:5 +00b6 CALLB 30 (INPUT), 4 # 72:5 00b7 JMP 0032 -00b8 CALLB COLOR, 0 # 75:1 -00b9 CALLB CLS, 0 # 76:1 +00b8 CALLB 7 (COLOR), 0 # 75:1 +00b9 CALLB 6 (CLS), 0 # 76:1 00ba PUSH$ "losses" # 77:45 00bb PUSH% 4 # 77:45 00bc PUSH% 1 # 77:43 @@ -241,10 +241,10 @@ Thanks for playing 00c5 PUSH% 1 # 77:15 00c6 PUSH$ "Score:" # 77:7 00c7 PUSH% 4 # 77:7 -00c8 CALLB PRINT, 14 # 77:1 -00c9 CALLB PRINT, 0 # 78:1 +00c8 CALLB 48 (PRINT), 14 # 77:1 +00c9 CALLB 48 (PRINT), 0 # 78:1 00ca PUSH$ "Thanks for playing" # 79:7 00cb PUSH% 4 # 79:7 -00cc CALLB PRINT, 2 # 79:1 -00cd CALLB PRINT, 0 # 80:1 +00cc CALLB 48 (PRINT), 2 # 79:1 +00cd CALLB 48 (PRINT), 0 # 80:1 diff --git a/cli/tests/examples/hello.out b/cli/tests/examples/hello.out index 5c94ff82..5bc6bf38 100644 --- a/cli/tests/examples/hello.out +++ b/cli/tests/examples/hello.out @@ -3,7 +3,7 @@ Hello,First-Name Last-Name! 0001 PUSH% 1 # 16:25 0002 PUSH$ "What's your name" # 16:7 0003 PUSH% 1 # 16:7 -0004 CALLB INPUT, 4 # 16:1 +0004 CALLB 30 (INPUT), 4 # 16:1 0005 LOAD$ NAME # 17:17 0006 PUSH$ "!" # 17:25 0007 CONCAT$ # 17:23 @@ -11,5 +11,5 @@ Hello,First-Name Last-Name! 0009 PUSH% 1 # 17:15 000a PUSH$ "Hello," # 17:7 000b PUSH% 4 # 17:7 -000c CALLB PRINT, 5 # 17:1 +000c CALLB 48 (PRINT), 5 # 17:1 diff --git a/cli/tests/examples/palette.out b/cli/tests/examples/palette.out index 6e2babc4..2f404470 100644 --- a/cli/tests/examples/palette.out +++ b/cli/tests/examples/palette.out @@ -1,11 +1,11 @@ [?25l 0  1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99  100  101  102  103  104  105  106  107  108  109  110  111  112  113  114  115  116  117  118  119  120  121  122  123  124  125  126  127  128  129  130  131  132  133  134  135  136  137  138  139  140  141  142  143  144  145  146  147  148  149  150  151  152  153  154  155  156  157  158  159  160  161  162  163  164  165  166  167  168  169  170  171  172  173  174  175  176  177  178  179  180  181  182  183  184  185  186  187  188  189  190  191  192  193  194  195  196  197  198  199  200  201  202  203  204  205  206  207  208  209  210  211  212  213  214  215  216  217  218  219  220  221  222  223  224  225  226  227  228  229  230  231  232  233  234  235  236  237  238  239  240  241  242  243  244  245  246  247  248  249  250  251  252  253  254  255 [?25h -0000 CALLB CLS, 0 # 20:1 +0000 CALLB 6 (CLS), 0 # 20:1 0001 PUSH% 0 # 21:7 0002 SETV ROW 0003 PUSH% 0 # 22:7 0004 SETV COL 0005 PUSH? false # 23:10 -0006 CALLB GFX_SYNC, 1 # 23:1 +0006 CALLB 22 (GFX_SYNC), 1 # 23:1 0007 PUSH% 0 # 24:9 0008 SETV C 0009 LOAD% C # 24:5 @@ -14,7 +14,7 @@ 000c JMPNT 0071 000d LOAD% ROW # 25:17 000e LOAD% COL # 25:12 -000f CALLB LOCATE, 2 # 25:5 +000f CALLB 38 (LOCATE), 2 # 25:5 0010 LOAD% C # 27:17 0011 SETV 0SELECT1 0012 LOAD% 0SELECT1 # 28:14 @@ -49,13 +49,13 @@ 002f PUSH% 1 # 28:56 0030 PUSH% 15 # 28:52 0031 PUSH% 1 # 28:52 -0032 CALLB COLOR, 4 # 28:46 +0032 CALLB 7 (COLOR), 4 # 28:46 0033 JMP 0039 0034 LOAD% C # 29:29 0035 PUSH% 1 # 29:29 0036 PUSH% 0 # 29:26 0037 PUSH% 1 # 29:26 -0038 CALLB COLOR, 4 # 29:20 +0038 CALLB 7 (COLOR), 4 # 29:20 0039 UNSETV 0SELECT1 # 30:5 003a LOAD% C # 32:17 003b SETV 0SELECT2 @@ -70,7 +70,7 @@ 0044 PUSH% 1 # 33:33 0045 PUSH$ " " # 33:29 0046 PUSH% 4 # 33:29 -0047 CALLB PRINT, 7 # 33:23 +0047 CALLB 48 (PRINT), 7 # 33:23 0048 JMP 005b 0049 LOAD% 0SELECT2 # 34:19 004a PUSH% 100 # 34:19 @@ -83,20 +83,20 @@ 0051 PUSH% 1 # 34:33 0052 PUSH$ " " # 34:30 0053 PUSH% 4 # 34:30 -0054 CALLB PRINT, 7 # 34:24 +0054 CALLB 48 (PRINT), 7 # 34:24 0055 JMP 005b 0056 PUSH% 0 # 35:28 0057 PUSH% 1 # 35:27 0058 LOAD% C # 35:26 0059 PUSH% 3 # 35:26 -005a CALLB PRINT, 4 # 35:20 +005a CALLB 48 (PRINT), 4 # 35:20 005b UNSETV 0SELECT2 # 36:5 005c LOAD% COL # 38:11 005d PUSH% 6 # 38:17 005e ADD% # 38:15 005f SETV COL 0060 LOAD% COL # 39:8 -0061 CALLF% SCRCOLS, 0 # 39:14 +0061 CALLF% 59 (SCRCOLS), 0 # 39:14 0062 PUSH% 5 # 39:24 0063 SUB% # 39:22 0064 CMPG% # 39:12 @@ -113,7 +113,7 @@ 006f SETV C 0070 JMP 0009 0071 PUSH? true # 44:10 -0072 CALLB GFX_SYNC, 1 # 44:1 -0073 CALLB COLOR, 0 # 46:1 -0074 CALLB PRINT, 0 # 47:1 +0072 CALLB 22 (GFX_SYNC), 1 # 44:1 +0073 CALLB 7 (COLOR), 0 # 46:1 +0074 CALLB 48 (PRINT), 0 # 47:1 diff --git a/cli/tests/examples/tour.out b/cli/tests/examples/tour.out index 8bc5de24..d0d7fefd 100644 --- a/cli/tests/examples/tour.out +++ b/cli/tests/examples/tour.out @@ -160,223 +160,223 @@ you know, to keep me motivated in writing stuff and building this project: 0001 CALLA 0167 0002 PUSH$ "Welcome to the EndBASIC tour demo program. I'm glad you have made it this far!" # 49:7 0003 PUSH% 4 # 49:7 -0004 CALLB PRINT, 2 # 49:1 -0005 CALLB PRINT, 0 # 50:1 +0004 CALLB 48 (PRINT), 2 # 49:1 +0005 CALLB 48 (PRINT), 0 # 50:1 0006 PUSH$ "EndBASIC is an interpreter for a BASIC-like language and is inspired by" # 51:7 0007 PUSH% 4 # 51:7 -0008 CALLB PRINT, 2 # 51:1 +0008 CALLB 48 (PRINT), 2 # 51:1 0009 PUSH$ "Amstrad's Locomotive BASIC 1.1 and Microsoft's QuickBASIC 4.5. The main idea" # 52:7 000a PUSH% 4 # 52:7 -000b CALLB PRINT, 2 # 52:1 +000b CALLB 48 (PRINT), 2 # 52:1 000c PUSH$ "behind EndBASIC is to provide a playground for learning the foundations of" # 53:7 000d PUSH% 4 # 53:7 -000e CALLB PRINT, 2 # 53:1 +000e CALLB 48 (PRINT), 2 # 53:1 000f PUSH$ "programming in a simplified environment." # 54:7 0010 PUSH% 4 # 54:7 -0011 CALLB PRINT, 2 # 54:1 -0012 CALLB PRINT, 0 # 55:1 +0011 CALLB 48 (PRINT), 2 # 54:1 +0012 CALLB 48 (PRINT), 0 # 55:1 0013 PUSH$ "EndBASIC is written in Rust and is proven to work on Linux, macOS and Windows." # 56:7 0014 PUSH% 4 # 56:7 -0015 CALLB PRINT, 2 # 56:1 +0015 CALLB 48 (PRINT), 2 # 56:1 0016 PUSH$ "It likely works on other Unix systems too. And, thanks to WASM, it also runs" # 57:7 0017 PUSH% 4 # 57:7 -0018 CALLB PRINT, 2 # 57:1 +0018 CALLB 48 (PRINT), 2 # 57:1 0019 PUSH$ "on the web--which I bet is how you are reading this right now." # 58:7 001a PUSH% 4 # 58:7 -001b CALLB PRINT, 2 # 58:1 -001c CALLB PRINT, 0 # 59:1 +001b CALLB 48 (PRINT), 2 # 58:1 +001c CALLB 48 (PRINT), 0 # 59:1 001d PUSH$ "If you are accessing EndBASIC via the web interface, please be aware that" # 60:7 001e PUSH% 4 # 60:7 -001f CALLB PRINT, 2 # 60:1 +001f CALLB 48 (PRINT), 2 # 60:1 0020 PUSH$ "this interface is highly experimental and has many rough edges. In particular," # 61:7 0021 PUSH% 4 # 61:7 -0022 CALLB PRINT, 2 # 61:1 +0022 CALLB 48 (PRINT), 2 # 61:1 0023 PUSH$ "things will go wrong if you try to resize the browser window. Just reload" # 62:7 0024 PUSH% 4 # 62:7 -0025 CALLB PRINT, 2 # 62:1 +0025 CALLB 48 (PRINT), 2 # 62:1 0026 PUSH$ "the page for a "reboot"." # 63:7 0027 PUSH% 4 # 63:7 -0028 CALLB PRINT, 2 # 63:1 -0029 CALLB PRINT, 0 # 64:1 +0028 CALLB 48 (PRINT), 2 # 63:1 +0029 CALLB 48 (PRINT), 0 # 64:1 002a PUSH% 9 # 65:7 -002b CALLB COLOR, 1 # 65:1 +002b CALLB 7 (COLOR), 1 # 65:1 002c PUSH$ "When not in the tour, use the HELP command to access the interactive help" # 66:7 002d PUSH% 4 # 66:7 -002e CALLB PRINT, 2 # 66:1 +002e CALLB 48 (PRINT), 2 # 66:1 002f PUSH$ "system." # 67:7 0030 PUSH% 4 # 67:7 -0031 CALLB PRINT, 2 # 67:1 -0032 CALLB COLOR, 0 # 68:1 -0033 CALLB PRINT, 0 # 69:1 +0031 CALLB 48 (PRINT), 2 # 67:1 +0032 CALLB 7 (COLOR), 0 # 68:1 +0033 CALLB 48 (PRINT), 0 # 69:1 0034 PUSH$ "Without further ado, let's get started!" # 70:7 0035 PUSH% 4 # 70:7 -0036 CALLB PRINT, 2 # 70:1 +0036 CALLB 48 (PRINT), 2 # 70:1 0037 CALLA 0191 0038 PUSH$ "Language basics" # 73:8 0039 CALLA 0167 003a PUSH$ "There are four primitive types: booleans (?), double-precision floating" # 74:7 003b PUSH% 4 # 74:7 -003c CALLB PRINT, 2 # 74:1 +003c CALLB 48 (PRINT), 2 # 74:1 003d PUSH$ "point numbers (#), 32-bit signed integers (%), and strings ($)." # 75:7 003e PUSH% 4 # 75:7 -003f CALLB PRINT, 2 # 75:1 -0040 CALLB PRINT, 0 # 76:1 +003f CALLB 48 (PRINT), 2 # 75:1 +0040 CALLB 48 (PRINT), 0 # 76:1 0041 PUSH$ "The common IF and SELECT CASE conditional structures, the DO, FOR, and WHILE" # 77:7 0042 PUSH% 4 # 77:7 -0043 CALLB PRINT, 2 # 77:1 +0043 CALLB 48 (PRINT), 2 # 77:1 0044 PUSH$ "loops, as well as GOSUB and GOTO are supported." # 78:7 0045 PUSH% 4 # 78:7 -0046 CALLB PRINT, 2 # 78:1 -0047 CALLB PRINT, 0 # 79:1 +0046 CALLB 48 (PRINT), 2 # 78:1 +0047 CALLB 48 (PRINT), 0 # 79:1 0048 PUSH$ "A trivial program to ask a question and print an answer would look like:" # 80:7 0049 PUSH% 4 # 80:7 -004a CALLB PRINT, 2 # 80:1 -004b CALLB PRINT, 0 # 81:1 +004a CALLB 48 (PRINT), 2 # 80:1 +004b CALLB 48 (PRINT), 0 # 81:1 004c PUSH$ " @retry: INPUT "Enter a number greater than 10: ", n" # 82:7 004d PUSH% 4 # 82:7 -004e CALLB PRINT, 2 # 82:1 +004e CALLB 48 (PRINT), 2 # 82:1 004f PUSH$ " IF n <= 10 THEN GOTO @retry" # 83:7 0050 PUSH% 4 # 83:7 -0051 CALLB PRINT, 2 # 83:1 +0051 CALLB 48 (PRINT), 2 # 83:1 0052 PUSH$ " PRINT "Good job!"" # 84:7 0053 PUSH% 4 # 84:7 -0054 CALLB PRINT, 2 # 84:1 -0055 CALLB PRINT, 0 # 85:1 +0054 CALLB 48 (PRINT), 2 # 84:1 +0055 CALLB 48 (PRINT), 0 # 85:1 0056 PUSH$ "Type HELP "LANG" for specific details about the language constructs." # 86:7 0057 PUSH% 4 # 86:7 -0058 CALLB PRINT, 2 # 86:1 +0058 CALLB 48 (PRINT), 2 # 86:1 0059 CALLA 0191 005a PUSH$ "File manipulation" # 89:8 005b CALLA 0167 005c PUSH$ "Given that you are reading this tour, you have already encountered how to" # 90:7 005d PUSH% 4 # 90:7 -005e CALLB PRINT, 2 # 90:1 +005e CALLB 48 (PRINT), 2 # 90:1 005f PUSH$ "load a program and run it. But here is how you'd go about creating a new" # 91:7 0060 PUSH% 4 # 91:7 -0061 CALLB PRINT, 2 # 91:1 +0061 CALLB 48 (PRINT), 2 # 91:1 0062 PUSH$ "program from scratch:" # 92:7 0063 PUSH% 4 # 92:7 -0064 CALLB PRINT, 2 # 92:1 -0065 CALLB PRINT, 0 # 93:1 +0064 CALLB 48 (PRINT), 2 # 92:1 +0065 CALLB 48 (PRINT), 0 # 93:1 0066 PUSH$ "1. Type NEW to clear the machine's program and variables." # 94:7 0067 PUSH% 4 # 94:7 -0068 CALLB PRINT, 2 # 94:1 +0068 CALLB 48 (PRINT), 2 # 94:1 0069 PUSH$ "2. Type EDIT to enter the full-screen editor." # 95:7 006a PUSH% 4 # 95:7 -006b CALLB PRINT, 2 # 95:1 +006b CALLB 48 (PRINT), 2 # 95:1 006c PUSH$ "3. Type your program in the editor and then press ESC to exit." # 96:7 006d PUSH% 4 # 96:7 -006e CALLB PRINT, 2 # 96:1 +006e CALLB 48 (PRINT), 2 # 96:1 006f PUSH$ "4. Optionally save your program with SAVE "some-name.bas"." # 97:7 0070 PUSH% 4 # 97:7 -0071 CALLB PRINT, 2 # 97:1 +0071 CALLB 48 (PRINT), 2 # 97:1 0072 PUSH$ "5. Run the program with RUN." # 98:7 0073 PUSH% 4 # 98:7 -0074 CALLB PRINT, 2 # 98:1 +0074 CALLB 48 (PRINT), 2 # 98:1 0075 PUSH$ "6. Repeat from 2 if things don't go as planned." # 99:7 0076 PUSH% 4 # 99:7 -0077 CALLB PRINT, 2 # 99:1 -0078 CALLB PRINT, 0 # 100:1 +0077 CALLB 48 (PRINT), 2 # 99:1 +0078 CALLB 48 (PRINT), 0 # 100:1 0079 PUSH$ "The cycle above works for demos too. You can LOAD any demo program and" # 101:7 007a PUSH% 4 # 101:7 -007b CALLB PRINT, 2 # 101:1 +007b CALLB 48 (PRINT), 2 # 101:1 007c PUSH$ "enter the interactive editor with EDIT to see and modify its code. What" # 102:7 007d PUSH% 4 # 102:7 -007e CALLB PRINT, 2 # 102:1 +007e CALLB 48 (PRINT), 2 # 102:1 007f PUSH$ "you cannot do is save them under their original name; you will have to pick" # 103:7 0080 PUSH% 4 # 103:7 -0081 CALLB PRINT, 2 # 103:1 +0081 CALLB 48 (PRINT), 2 # 103:1 0082 PUSH$ "a different name." # 104:7 0083 PUSH% 4 # 104:7 -0084 CALLB PRINT, 2 # 104:1 -0085 CALLB PRINT, 0 # 105:1 +0084 CALLB 48 (PRINT), 2 # 104:1 +0085 CALLB 48 (PRINT), 0 # 105:1 0086 PUSH$ "If you are in the browser, rest assured that all programs are stored in" # 106:7 0087 PUSH% 4 # 106:7 -0088 CALLB PRINT, 2 # 106:1 +0088 CALLB 48 (PRINT), 2 # 106:1 0089 PUSH$ "your browser's local storage. Nothing goes to the cloud." # 107:7 008a PUSH% 4 # 107:7 -008b CALLB PRINT, 2 # 107:1 +008b CALLB 48 (PRINT), 2 # 107:1 008c CALLA 0191 008d PUSH$ "The file system" # 110:8 008e CALLA 0167 008f PUSH$ "In the previous page, you learned how to create files and how to save and" # 111:7 0090 PUSH% 4 # 111:7 -0091 CALLB PRINT, 2 # 111:1 +0091 CALLB 48 (PRINT), 2 # 111:1 0092 PUSH$ "load them. Those examples used relative paths. However, EndBASIC supports" # 112:7 0093 PUSH% 4 # 112:7 -0094 CALLB PRINT, 2 # 112:1 +0094 CALLB 48 (PRINT), 2 # 112:1 0095 PUSH$ "multiple drives (although it does not yet support directories)." # 113:7 0096 PUSH% 4 # 113:7 -0097 CALLB PRINT, 2 # 113:1 -0098 CALLB PRINT, 0 # 114:1 +0097 CALLB 48 (PRINT), 2 # 113:1 +0098 CALLB 48 (PRINT), 0 # 114:1 0099 PUSH$ "Paths in EndBASIC have the form DRIVE:FILE or DRIVE:/FILE. Given that" # 115:7 009a PUSH% 4 # 115:7 -009b CALLB PRINT, 2 # 115:1 +009b CALLB 48 (PRINT), 2 # 115:1 009c PUSH$ "directories are not yet supported, both are equivalent, but their meaning" # 116:7 009d PUSH% 4 # 116:7 -009e CALLB PRINT, 2 # 116:1 +009e CALLB 48 (PRINT), 2 # 116:1 009f PUSH$ "might change in the future. All commands that operate on paths accept these" # 117:7 00a0 PUSH% 4 # 117:7 -00a1 CALLB PRINT, 2 # 117:1 +00a1 CALLB 48 (PRINT), 2 # 117:1 00a2 PUSH$ "syntaxes. Note that the DRIVE: part is optional: when not specified, the" # 118:7 00a3 PUSH% 4 # 118:7 -00a4 CALLB PRINT, 2 # 118:1 +00a4 CALLB 48 (PRINT), 2 # 118:1 00a5 PUSH$ "current drive (shown by the DIR command) will be used." # 119:7 00a6 PUSH% 4 # 119:7 -00a7 CALLB PRINT, 2 # 119:1 -00a8 CALLB PRINT, 0 # 120:1 +00a7 CALLB 48 (PRINT), 2 # 119:1 +00a8 CALLB 48 (PRINT), 0 # 120:1 00a9 PUSH$ "You can use the MOUNT command to display the list of currently-mounted drives" # 121:7 00aa PUSH% 4 # 121:7 -00ab CALLB PRINT, 2 # 121:1 +00ab CALLB 48 (PRINT), 2 # 121:1 00ac PUSH$ "and to attach new ones. Pay attention to the default MOUNT output as it" # 122:7 00ad PUSH% 4 # 122:7 -00ae CALLB PRINT, 2 # 122:1 +00ae CALLB 48 (PRINT), 2 # 122:1 00af PUSH$ "shows some of the possible URIs you can use to mount other drives." # 123:7 00b0 PUSH% 4 # 123:7 -00b1 CALLB PRINT, 2 # 123:1 +00b1 CALLB 48 (PRINT), 2 # 123:1 00b2 PUSH$ "For example, if you want to gain access to an arbitrary directory in the" # 124:7 00b3 PUSH% 4 # 124:7 -00b4 CALLB PRINT, 2 # 124:1 +00b4 CALLB 48 (PRINT), 2 # 124:1 00b5 PUSH$ "system, you could do:" # 125:7 00b6 PUSH% 4 # 125:7 -00b7 CALLB PRINT, 2 # 125:1 -00b8 CALLB PRINT, 0 # 126:1 +00b7 CALLB 48 (PRINT), 2 # 125:1 +00b8 CALLB 48 (PRINT), 0 # 126:1 00b9 PUSH$ " MOUNT "TMP", "file:///PATH/TO/TMPDIR"" # 127:7 00ba PUSH% 4 # 127:7 -00bb CALLB PRINT, 2 # 127:1 +00bb CALLB 48 (PRINT), 2 # 127:1 00bc PUSH$ " CD "TMP:/"" # 128:7 00bd PUSH% 4 # 128:7 -00be CALLB PRINT, 2 # 128:1 -00bf CALLB PRINT, 0 # 129:1 +00be CALLB 48 (PRINT), 2 # 128:1 +00bf CALLB 48 (PRINT), 0 # 129:1 00c0 PUSH$ "Pay attention to the double quotes surrounding these arguments: these are" # 130:7 00c1 PUSH% 4 # 130:7 -00c2 CALLB PRINT, 2 # 130:1 +00c2 CALLB 48 (PRINT), 2 # 130:1 00c3 PUSH$ "EndBASIC commands and thus you must provide the arguments as strings. You" # 131:7 00c4 PUSH% 4 # 131:7 -00c5 CALLB PRINT, 2 # 131:1 +00c5 CALLB 48 (PRINT), 2 # 131:1 00c6 PUSH$ "are bound to trip over this a few times due to muscle memory..." # 132:7 00c7 PUSH% 4 # 132:7 -00c8 CALLB PRINT, 2 # 132:1 +00c8 CALLB 48 (PRINT), 2 # 132:1 00c9 CALLA 0191 00ca PUSH$ "Screen manipulation" # 135:8 00cb CALLA 0167 00cc PUSH$ "You have several commands at your disposal to manipulate the contents of" # 136:7 00cd PUSH% 4 # 136:7 -00ce CALLB PRINT, 2 # 136:1 +00ce CALLB 48 (PRINT), 2 # 136:1 00cf PUSH$ "the screen. Visual features are particularly interesting for teaching" # 137:7 00d0 PUSH% 4 # 137:7 -00d1 CALLB PRINT, 2 # 137:1 +00d1 CALLB 48 (PRINT), 2 # 137:1 00d2 PUSH$ "purposes, so expect more in this regard." # 138:7 00d3 PUSH% 4 # 138:7 -00d4 CALLB PRINT, 2 # 138:1 -00d5 CALLB PRINT, 0 # 139:1 +00d4 CALLB 48 (PRINT), 2 # 138:1 +00d5 CALLB 48 (PRINT), 0 # 139:1 00d6 PUSH$ "For example, we can print the foundational colors by selecting them with" # 140:7 00d7 PUSH% 4 # 140:7 -00d8 CALLB PRINT, 2 # 140:1 +00d8 CALLB 48 (PRINT), 2 # 140:1 00d9 PUSH$ "the "COLOR" command and positioning the cursor with "LOCATE":" # 141:7 00da PUSH% 4 # 141:7 -00db CALLB PRINT, 2 # 141:1 -00dc CALLB PRINT, 0 # 142:1 +00db CALLB 48 (PRINT), 2 # 141:1 +00dc CALLB 48 (PRINT), 0 # 142:1 00dd PUSH% 0 # 143:10 00de SETV C 00df LOAD% C # 143:5 @@ -387,15 +387,15 @@ you know, to keep me motivated in writing stuff and building this project: 00e4 LOAD% C # 144:20 00e5 ADD% # 144:18 00e6 PUSH% 4 # 144:12 -00e7 CALLB LOCATE, 2 # 144:5 +00e7 CALLB 38 (LOCATE), 2 # 144:5 00e8 LOAD% C # 145:11 -00e9 CALLB COLOR, 1 # 145:5 +00e9 CALLB 7 (COLOR), 1 # 145:5 00ea LOAD% C # 146:28 00eb PUSH% 3 # 146:28 00ec PUSH% 1 # 146:26 00ed PUSH$ "This is color" # 146:11 00ee PUSH% 4 # 146:11 -00ef CALLB PRINT, 5 # 146:5 +00ef CALLB 48 (PRINT), 5 # 146:5 00f0 LOAD% C # 143:5 00f1 PUSH% 1 # 143:16 00f2 ADD% # 143:12 @@ -413,116 +413,116 @@ you know, to keep me motivated in writing stuff and building this project: 00fe PUSH% 8 # 149:26 00ff SUB% # 149:24 0100 PUSH% 23 # 149:12 -0101 CALLB LOCATE, 2 # 149:5 +0101 CALLB 38 (LOCATE), 2 # 149:5 0102 LOAD% C # 150:11 -0103 CALLB COLOR, 1 # 150:5 +0103 CALLB 7 (COLOR), 1 # 150:5 0104 LOAD% C # 151:28 0105 PUSH% 3 # 151:28 0106 PUSH% 1 # 151:26 0107 PUSH$ "This is color" # 151:11 0108 PUSH% 4 # 151:11 -0109 CALLB PRINT, 5 # 151:5 +0109 CALLB 48 (PRINT), 5 # 151:5 010a LOAD% C # 148:5 010b PUSH% 1 # 148:17 010c ADD% # 148:12 010d SETV C 010e JMP 00f7 -010f CALLB COLOR, 0 # 153:1 +010f CALLB 7 (COLOR), 0 # 153:1 0110 CALLA 0191 0111 PUSH$ "Hardware access" # 156:8 0112 CALLA 0167 0113 PUSH$ "If you happen to be running on a Raspberry Pi, EndBASIC has some support" # 157:7 0114 PUSH% 4 # 157:7 -0115 CALLB PRINT, 2 # 157:1 +0115 CALLB 48 (PRINT), 2 # 157:1 0116 PUSH$ "to manipulate its hardware. At the moment this includes only basic access" # 158:7 0117 PUSH% 4 # 158:7 -0118 CALLB PRINT, 2 # 158:1 +0118 CALLB 48 (PRINT), 2 # 158:1 0119 PUSH$ "to the GPIO lines. See the "DEMOS:/GPIO.BAS" demo for an example." # 159:7 011a PUSH% 4 # 159:7 -011b CALLB PRINT, 2 # 159:1 -011c CALLB PRINT, 0 # 160:1 +011b CALLB 48 (PRINT), 2 # 159:1 +011c CALLB 48 (PRINT), 0 # 160:1 011d PUSH$ "Please note that you have to be running on a Raspberry Pi *AND* you must" # 161:7 011e PUSH% 4 # 161:7 -011f CALLB PRINT, 2 # 161:1 +011f CALLB 48 (PRINT), 2 # 161:1 0120 PUSH$ "have compiled EndBASIC with --features=rpi for this to work." # 162:7 0121 PUSH% 4 # 162:7 -0122 CALLB PRINT, 2 # 162:1 +0122 CALLB 48 (PRINT), 2 # 162:1 0123 CALLA 0191 0124 PUSH$ "Enjoy" # 165:8 0125 CALLA 0167 0126 PUSH$ "And that's it for the tour. You can now type EDIT to see the code that" # 166:7 0127 PUSH% 4 # 166:7 -0128 CALLB PRINT, 2 # 166:1 +0128 CALLB 48 (PRINT), 2 # 166:1 0129 PUSH$ "took you over this journey, load other demo files or... just go forth and" # 167:7 012a PUSH% 4 # 167:7 -012b CALLB PRINT, 2 # 167:1 +012b CALLB 48 (PRINT), 2 # 167:1 012c PUSH$ "explore. HELP, MOUNT, and DIR are your friends at any point, but so that" # 168:7 012d PUSH% 4 # 168:7 -012e CALLB PRINT, 2 # 168:1 +012e CALLB 48 (PRINT), 2 # 168:1 012f PUSH$ "you don't feel too lost, run this now:" # 169:7 0130 PUSH% 4 # 169:7 -0131 CALLB PRINT, 2 # 169:1 -0132 CALLB PRINT, 0 # 170:1 +0131 CALLB 48 (PRINT), 2 # 169:1 +0132 CALLB 48 (PRINT), 0 # 170:1 0133 PUSH% 1 # 171:7 -0134 CALLB COLOR, 1 # 171:1 +0134 CALLB 7 (COLOR), 1 # 171:1 0135 PUSH$ " CD "DEMOS:/"" # 172:7 0136 PUSH% 4 # 172:7 -0137 CALLB PRINT, 2 # 172:1 +0137 CALLB 48 (PRINT), 2 # 172:1 0138 PUSH$ " DIR" # 173:7 0139 PUSH% 4 # 173:7 -013a CALLB PRINT, 2 # 173:1 -013b CALLB COLOR, 0 # 174:1 -013c CALLB PRINT, 0 # 175:1 +013a CALLB 48 (PRINT), 2 # 173:1 +013b CALLB 7 (COLOR), 0 # 174:1 +013c CALLB 48 (PRINT), 0 # 175:1 013d PUSH$ "If you like what you have seen so far, please head to the project's GitHub" # 176:7 013e PUSH% 4 # 176:7 -013f CALLB PRINT, 2 # 176:1 +013f CALLB 48 (PRINT), 2 # 176:1 0140 PUSH$ "page and give it a star:" # 177:7 0141 PUSH% 4 # 177:7 -0142 CALLB PRINT, 2 # 177:1 +0142 CALLB 48 (PRINT), 2 # 177:1 0143 PUSH% 12 # 178:7 -0144 CALLB COLOR, 1 # 178:1 -0145 CALLB PRINT, 0 # 179:1 +0144 CALLB 7 (COLOR), 1 # 178:1 +0145 CALLB 48 (PRINT), 0 # 179:1 0146 PUSH$ " https://github.com/endbasic/endbasic/" # 180:7 0147 PUSH% 4 # 180:7 -0148 CALLB PRINT, 2 # 180:1 -0149 CALLB PRINT, 0 # 181:1 -014a CALLB COLOR, 0 # 182:1 +0148 CALLB 48 (PRINT), 2 # 180:1 +0149 CALLB 48 (PRINT), 0 # 181:1 +014a CALLB 7 (COLOR), 0 # 182:1 014b PUSH$ "Then, visit my blog and subscribe to receive fresh EndBASIC content or..." # 183:7 014c PUSH% 4 # 183:7 -014d CALLB PRINT, 2 # 183:1 +014d CALLB 48 (PRINT), 2 # 183:1 014e PUSH$ "you know, to keep me motivated in writing stuff and building this project:" # 184:7 014f PUSH% 4 # 184:7 -0150 CALLB PRINT, 2 # 184:1 +0150 CALLB 48 (PRINT), 2 # 184:1 0151 PUSH% 12 # 185:7 -0152 CALLB COLOR, 1 # 185:1 -0153 CALLB PRINT, 0 # 186:1 +0152 CALLB 7 (COLOR), 1 # 185:1 +0153 CALLB 48 (PRINT), 0 # 186:1 0154 PUSH$ " https://jmmv.dev/" # 187:7 0155 PUSH% 4 # 187:7 -0156 CALLB PRINT, 2 # 187:1 -0157 CALLB PRINT, 0 # 188:1 -0158 CALLB COLOR, 0 # 189:1 +0156 CALLB 48 (PRINT), 2 # 187:1 +0157 CALLB 48 (PRINT), 0 # 188:1 +0158 CALLB 7 (COLOR), 0 # 189:1 0159 PUSH$ "Thank you! :-)" # 190:7 015a PUSH% 4 # 190:7 -015b CALLB PRINT, 2 # 190:1 -015c CALLB PRINT, 0 # 191:1 +015b CALLB 48 (PRINT), 2 # 190:1 +015c CALLB 48 (PRINT), 0 # 191:1 015d PUSH% 10 # 192:7 -015e CALLB COLOR, 1 # 192:1 +015e CALLB 7 (COLOR), 1 # 192:1 015f PUSH$ "-- Brought to you by Julio Merino " # 193:7 0160 PUSH% 4 # 193:7 -0161 CALLB PRINT, 2 # 193:1 +0161 CALLB 48 (PRINT), 2 # 193:1 0162 PUSH? true # 196:10 -0163 CALLB GFX_SYNC, 1 # 196:1 -0164 CALLB COLOR, 0 # 197:1 -0165 CALLB PRINT, 0 # 198:1 +0163 CALLB 22 (GFX_SYNC), 1 # 196:1 +0164 CALLB 7 (COLOR), 0 # 197:1 +0165 CALLB 48 (PRINT), 0 # 198:1 0166 JMP 01b0 0167 ENTER 0168 SETV TITLE -0169 CALLB CLS, 0 # 18:5 +0169 CALLB 6 (CLS), 0 # 18:5 016a PUSH? false # 19:14 -016b CALLB GFX_SYNC, 1 # 19:5 +016b CALLB 22 (GFX_SYNC), 1 # 19:5 016c PUSH% 11 # 20:11 -016d CALLB COLOR, 1 # 20:5 -016e CALLB PRINT, 0 # 21:5 +016d CALLB 7 (COLOR), 1 # 20:5 +016e CALLB 48 (PRINT), 0 # 21:5 016f PUSH$ " EndBASIC tour: " # 22:14 0170 LOAD$ TITLE # 22:35 0171 CONCAT$ # 22:33 @@ -533,7 +533,7 @@ you know, to keep me motivated in writing stuff and building this project: 0176 SETV I 0177 LOAD% I # 24:9 0178 LOAD$ TITLE # 24:23 -0179 CALLF% LEN, 1 # 24:18 +0179 CALLF% 35 (LEN), 1 # 24:18 017a PUSH% 1 # 24:33 017b ADD% # 24:31 017c CMPLE% # 24:15 @@ -549,26 +549,26 @@ you know, to keep me motivated in writing stuff and building this project: 0186 JMP 0177 0187 LOAD$ TITLE # 27:11 0188 PUSH% 4 # 27:11 -0189 CALLB PRINT, 2 # 27:5 +0189 CALLB 48 (PRINT), 2 # 27:5 018a LOAD$ UNDERLINE # 28:11 018b PUSH% 4 # 28:11 -018c CALLB PRINT, 2 # 28:5 -018d CALLB COLOR, 0 # 29:5 -018e CALLB PRINT, 0 # 30:5 +018c CALLB 48 (PRINT), 2 # 28:5 +018d CALLB 7 (COLOR), 0 # 29:5 +018e CALLB 48 (PRINT), 0 # 30:5 018f LEAVE 0190 RET # 31:1 0191 ENTER -0192 CALLB PRINT, 0 # 35:5 +0192 CALLB 48 (PRINT), 0 # 35:5 0193 PUSH% 11 # 36:11 -0194 CALLB COLOR, 1 # 36:5 +0194 CALLB 7 (COLOR), 1 # 36:5 0195 PUSH% 0 # 37:64 0196 PUSH% 1 # 37:63 0197 PUSH$ "Press ENTER to continue or ESC to exit the demo..." # 37:11 0198 PUSH% 4 # 37:11 -0199 CALLB PRINT, 4 # 37:5 +0199 CALLB 48 (PRINT), 4 # 37:5 019a PUSH? true # 38:14 -019b CALLB GFX_SYNC, 1 # 38:5 -019c CALLF$ INKEY, 0 # 40:21 +019b CALLB 22 (GFX_SYNC), 1 # 38:5 +019c CALLF$ 29 (INKEY), 0 # 40:21 019d SETV 0SELECT1 019e LOAD$ 0SELECT1 # 41:14 019f PUSH$ "ENTER" # 41:14 @@ -583,7 +583,7 @@ you know, to keep me motivated in writing stuff and building this project: 01a8 JMP 0162 01a9 JMP 01ac 01aa PUSH# 0.01 # 43:26 -01ab CALLB SLEEP, 1 # 43:20 +01ab CALLB 64 (SLEEP), 1 # 43:20 01ac UNSETV 0SELECT1 # 44:9 01ad JMP 019c 01ae LEAVE diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index dc719fd4..e8e01f35 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -32,6 +32,9 @@ pub struct BuiltinCallISpan { /// Position of the name. pub name_pos: LineCol, + /// Runtime index to execute this call. + pub upcall_index: usize, + /// Number of arguments on the stack for the call. /// /// The arguments in the stack are interspersed with the separators used to separate them from @@ -86,6 +89,9 @@ pub struct FunctionCallISpan { /// Position of the name. pub name_pos: LineCol, + /// Runtime index to execute this call. + pub upcall_index: usize, + /// Return type of the function. pub return_type: ExprType, @@ -434,7 +440,7 @@ impl Instruction { Instruction::Assign(key) => ("SETV", Some(key.to_string())), Instruction::BuiltinCall(span) => { - ("CALLB", Some(format!("{}, {}", span.name, span.nargs))) + ("CALLB", Some(format!("{} ({}), {}", span.upcall_index, span.name, span.nargs))) } Instruction::Call(span) => ("CALLA", Some(format!("{:04x}", span.addr))), @@ -446,7 +452,7 @@ impl Instruction { ExprType::Integer => "CALLF%", ExprType::Text => "CALLF$", }; - (opcode, Some(format!("{}, {}", span.name, span.nargs))) + (opcode, Some(format!("{} ({}), {}", span.upcall_index, span.name, span.nargs))) } Instruction::Dim(span) => { diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index d5dc5f1e..9c74204c 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -393,7 +393,7 @@ fn compile_required_ref( Ok(None) } - Some(SymbolPrototype::BuiltinCallable(md)) + Some(SymbolPrototype::BuiltinCallable(md, _)) | Some(SymbolPrototype::Callable(md)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 03f6cb1c..a6c5caea 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -378,7 +378,7 @@ fn compile_expr_symbol( } } - Some(SymbolPrototype::BuiltinCallable(md)) => { + Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { let etype = match md.return_type() { Some(etype) => etype, None => { @@ -396,6 +396,7 @@ fn compile_expr_symbol( Instruction::FunctionCall(FunctionCallISpan { name: key, name_pos: span.pos, + upcall_index: *upcall_index, return_type: etype, nargs: 0, }), @@ -704,7 +705,7 @@ pub(super) fn compile_expr( compile_array_ref(instrs, fixups, symtable, span, key, *vtype, *dims) } - Some(SymbolPrototype::BuiltinCallable(md)) => { + Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { if !span.vref.accepts_callable(md.return_type()) { return Err(Error::IncompatibleTypeAnnotationInReference( span.vref_pos, @@ -729,6 +730,7 @@ pub(super) fn compile_expr( instrs.push(Instruction::FunctionCall(FunctionCallISpan { name: key, name_pos: span_pos, + upcall_index: *upcall_index, return_type: vtype, nargs, })); @@ -852,6 +854,7 @@ mod tests { Instruction::FunctionCall(FunctionCallISpan { name: SymbolKey::from("f"), name_pos: lc(1, 5), + upcall_index: 0, return_type: ExprType::Integer, nargs: 0, }), @@ -1000,6 +1003,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("C"), name_pos: lc(1, 1), + upcall_index: 0, nargs: 1, }), ) @@ -1330,6 +1334,7 @@ mod tests { Instruction::FunctionCall(FunctionCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(1, 5), + upcall_index: 0, return_type: ExprType::Integer, nargs: 3, }), diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index e5f6a8c3..e5659dbb 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -118,7 +118,8 @@ enum SymbolPrototype { Array(ExprType, usize), /// Information about a callable that's a builtin and requires an upcall to execute. - BuiltinCallable(CallableMetadata), + /// The integer indicates the runtime upcall index of the callable. + BuiltinCallable(CallableMetadata, usize), /// Information about a callable. Callable(CallableMetadata), @@ -162,11 +163,24 @@ impl From> for SymbolsTable { impl From<&Symbols> for SymbolsTable { fn from(syms: &Symbols) -> Self { - let mut globals = HashMap::default(); - for (name, callable) in syms.callables() { - let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone()); - globals.insert(name.clone(), proto); - } + let globals = { + let mut globals = HashMap::default(); + + let callables = syms.callables(); + let mut names = callables.keys().copied().collect::>(); + // This is only necessary for testing really... but may also remove some confusion + // when inspecting the bytecode because it helps keep upcall indexes stable across + // different compilations. + names.sort(); + + for (i, name) in names.into_iter().enumerate() { + let callable = callables.get(&name).unwrap(); + let proto = SymbolPrototype::BuiltinCallable(callable.metadata().clone(), i); + globals.insert(name.clone(), proto); + } + + globals + }; let mut scope = HashMap::default(); for (name, symbol) in syms.locals() { @@ -222,6 +236,22 @@ impl SymbolsTable { debug_assert!(previous.is_none(), "Cannot redefine a symbol"); } + /// Inserts the builtin callable described by `md` and assigns an upcall index. + /// The symbol must not yet exist. + #[cfg(test)] + fn insert_builtin_callable(&mut self, key: SymbolKey, md: CallableMetadata) { + let next_upcall_index = self + .globals + .values() + .filter(|proto| matches!(proto, SymbolPrototype::BuiltinCallable(..))) + .count(); + + debug_assert!(!self.globals.contains_key(&key), "Cannot redefine a symbol"); + let proto = SymbolPrototype::BuiltinCallable(md, next_upcall_index); + let previous = self.globals.insert(key, proto); + debug_assert!(previous.is_none(), "Cannot redefine a symbol"); + } + /// Inserts the new information `proto` about symbol `key` into the symbols table. /// The symbol must not yet exist. fn insert_global(&mut self, key: SymbolKey, proto: SymbolPrototype) { @@ -847,19 +877,19 @@ impl Compiler { Statement::Call(span) => { let key = SymbolKey::from(&span.vref.name()); - let (md, is_builtin) = match self.symtable.get(&key) { - Some(SymbolPrototype::BuiltinCallable(md)) => { + let (md, upcall_index) = match self.symtable.get(&key) { + Some(SymbolPrototype::BuiltinCallable(md, upcall_index)) => { if md.is_function() { return Err(Error::NotACommand(span.vref_pos, span.vref)); } - (md.clone(), true) + (md.clone(), Some(*upcall_index)) } Some(SymbolPrototype::Callable(md)) => { if md.is_function() { return Err(Error::NotACommand(span.vref_pos, span.vref)); } - (md.clone(), false) + (md.clone(), None) } Some(_) => { @@ -878,10 +908,11 @@ impl Compiler { name_pos, span.args, )?; - if is_builtin { + if let Some(upcall_index) = upcall_index { self.emit(Instruction::BuiltinCall(BuiltinCallISpan { name: key, name_pos: span.vref_pos, + upcall_index, nargs, })); } else { @@ -1247,7 +1278,7 @@ mod testutils { pub(crate) fn define_callable(mut self, builder: CallableMetadataBuilder) -> Self { let md = builder.test_build(); let key = SymbolKey::from(md.name()); - self.symtable.insert(key, SymbolPrototype::BuiltinCallable(md)); + self.symtable.insert_builtin_callable(key, md); self } @@ -1583,6 +1614,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("CMD"), name_pos: lc(1, 1), + upcall_index: 0, nargs: 0, }), ) @@ -1613,6 +1645,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("CMD"), name_pos: lc(1, 15), + upcall_index: 0, nargs: 2, }), ) @@ -1884,6 +1917,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(2, 1), + upcall_index: 0, nargs: 0, }), ) @@ -1904,6 +1938,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(2, 1), + upcall_index: 0, nargs: 0, }), ) @@ -1922,6 +1957,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(2, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2502,6 +2538,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(2, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2532,6 +2569,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(1, 16), + upcall_index: 0, nargs: 0, }), ) @@ -2553,6 +2591,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(2, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2564,6 +2603,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("BAR"), name_pos: lc(4, 1), + upcall_index: 1, nargs: 0, }), ) @@ -2575,6 +2615,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("BAZ"), name_pos: lc(6, 1), + upcall_index: 2, nargs: 0, }), ) @@ -2639,6 +2680,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(3, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2790,6 +2832,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(3, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2831,6 +2874,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(3, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2859,6 +2903,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(3, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2872,6 +2917,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("BAR"), name_pos: lc(5, 1), + upcall_index: 1, nargs: 0, }), ) @@ -2900,6 +2946,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(3, 1), + upcall_index: 0, nargs: 0, }), ) @@ -2909,6 +2956,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("BAR"), name_pos: lc(5, 1), + upcall_index: 1, nargs: 0, }), ) @@ -2958,6 +3006,7 @@ mod tests { Instruction::BuiltinCall(BuiltinCallISpan { name: SymbolKey::from("FOO"), name_pos: lc(2, 1), + upcall_index: 0, nargs: 0, }), ) From 43b7dad7c16a0f8f9ed37b76cd89946a00b3c8df Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 9 Jan 2026 04:57:01 -0800 Subject: [PATCH 076/110] Use indexes for upcall execution This removes the need for a hashmap lookup on every builtin invocation and replaces it with a simple vector indexing operation. --- core/src/bytecode.rs | 3 ++ core/src/compiler/mod.rs | 30 ++++++++++++++++-- core/src/exec.rs | 66 +++++++++++++++++++++++----------------- 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/core/src/bytecode.rs b/core/src/bytecode.rs index e8e01f35..ac36291e 100644 --- a/core/src/bytecode.rs +++ b/core/src/bytecode.rs @@ -708,6 +708,9 @@ impl Instruction { /// Representation of a compiled program. #[cfg_attr(test, derive(Debug, PartialEq))] pub struct Image { + /// List of builtin upcalls in the order in which they were assigned indexes. + pub upcalls: Vec, + /// Collection of instructions in the program. /// /// The indices of this vector correspond to the program counter. diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index e5659dbb..7dbd1cad 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -273,6 +273,24 @@ impl SymbolsTable { keys.extend(self.scopes.last().unwrap().keys()); keys } + + /// Calculates the list of upcalls in this symbols table in the order in which they were + /// assigned indexes. + fn upcalls(&self) -> Vec { + let mut builtins = self + .globals + .iter() + .filter_map(|(key, proto)| { + if let SymbolPrototype::BuiltinCallable(_md, upcall_index) = proto { + Some((upcall_index, key)) + } else { + None + } + }) + .collect::>(); + builtins.sort_by_key(|(upcall_index, _key)| *upcall_index); + builtins.into_iter().map(|(_upcall_index, key)| key.clone()).collect() + } } /// Describes a location in the code needs fixing up after all addresses have been laid out. @@ -1221,7 +1239,9 @@ impl Compiler { ); self.instrs[pc] = new_instr; } - let image = Image { instrs: self.instrs, data: self.data }; + + let image = + Image { upcalls: self.symtable.upcalls(), instrs: self.instrs, data: self.data }; Ok((image, self.symtable)) } } @@ -1259,6 +1279,7 @@ mod testutils { pub(crate) struct Tester { source: String, symtable: SymbolsTable, + exp_upcalls: Vec, } impl Tester { @@ -1278,7 +1299,8 @@ mod testutils { pub(crate) fn define_callable(mut self, builder: CallableMetadataBuilder) -> Self { let md = builder.test_build(); let key = SymbolKey::from(md.name()); - self.symtable.insert_builtin_callable(key, md); + self.symtable.insert_builtin_callable(key.clone(), md); + self.exp_upcalls.push(key); self } @@ -1296,6 +1318,7 @@ mod testutils { result: compile_aux(&mut self.source.as_bytes(), self.symtable), exp_error: None, ignore_instrs: false, + exp_upcalls: self.exp_upcalls, exp_instrs: vec![], exp_data: vec![], exp_symtable: HashMap::default(), @@ -1309,6 +1332,7 @@ mod testutils { result: Result<(Image, SymbolsTable)>, exp_error: Option, ignore_instrs: bool, + exp_upcalls: Vec, exp_instrs: Vec, exp_data: Vec>, exp_symtable: HashMap, @@ -1364,6 +1388,8 @@ mod testutils { } let (image, symtable) = self.result.unwrap(); + assert_eq!(self.exp_upcalls, symtable.upcalls()); + if self.ignore_instrs { assert!( self.exp_instrs.is_empty(), diff --git a/core/src/exec.rs b/core/src/exec.rs index 8545869b..cd4d6067 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -88,6 +88,9 @@ pub enum Signal { /// Request to exit the VM execution loop to execute a native command or function. #[derive(Clone, Debug, Eq, PartialEq)] struct UpcallData { + /// Index of the upcall to execute. + index: usize, + /// Name of the callable to execute. name: SymbolKey, @@ -687,22 +690,16 @@ impl Machine { async fn builtin_call( &mut self, context: &mut Context, - name: &SymbolKey, + callable: Rc, bref_pos: LineCol, nargs: usize, ) -> Result<()> { - let b = match self.symbols.load(name) { - Some(Symbol::Callable(b)) => b, - _ => panic!("Command existence and type checking happen at compile time"), - }; - - let metadata = b.metadata(); + let metadata = callable.metadata(); debug_assert!(!metadata.is_function()); let scope = Scope::new(&mut context.value_stack, nargs, bref_pos); - let b = b.clone(); - b.exec(scope, self).await + callable.exec(scope, self).await } /// Handles an array definition. The array must not yet exist, and the name may not overlap @@ -956,27 +953,22 @@ impl Machine { async fn function_call( &mut self, context: &mut Context, + callable: Rc, name: &SymbolKey, return_type: ExprType, fref_pos: LineCol, nargs: usize, ) -> Result<()> { - match self.symbols.load(name) { - Some(Symbol::Callable(f)) => { - if !f.metadata().is_function() { - return Err(Error::EvalError( - fref_pos, - format!("{} is not an array nor a function", f.metadata().name()), - )); - } - let f = f.clone(); - if f.metadata().is_argless() { - self.argless_function_call(context, name, return_type, fref_pos, f).await - } else { - self.do_function_call(context, return_type, fref_pos, nargs, f).await - } - } - _ => unreachable!("Function existence and type checking has been done at compile time"), + if !callable.metadata().is_function() { + return Err(Error::EvalError( + fref_pos, + format!("{} is not an array nor a function", callable.metadata().name()), + )); + } + if callable.metadata().is_argless() { + self.argless_function_call(context, name, return_type, fref_pos, callable).await + } else { + self.do_function_call(context, return_type, fref_pos, nargs, callable).await } } @@ -1272,6 +1264,7 @@ impl Machine { Instruction::BuiltinCall(span) => { return Ok(InternalStopReason::Upcall(UpcallData { + index: span.upcall_index, name: span.name.clone(), return_type: None, pos: span.name_pos, @@ -1294,6 +1287,7 @@ impl Machine { Instruction::FunctionCall(span) => { return Ok(InternalStopReason::Upcall(UpcallData { + index: span.upcall_index, name: span.name.clone(), return_type: Some(span.return_type), pos: span.name_pos, @@ -1515,7 +1509,11 @@ impl Machine { /// Executes the instructions given in `instr`. /// /// This is a helper to `exec`, which prepares the machine with the program's data upfront. - async fn exec_with_data(&mut self, instrs: &[Instruction]) -> Result { + async fn exec_with_data( + &mut self, + upcalls: &[Rc], + instrs: &[Instruction], + ) -> Result { let mut context = Context::default(); while context.pc < instrs.len() { match self.exec_until_stop(&mut context, instrs) { @@ -1526,11 +1524,14 @@ impl Machine { } Ok(InternalStopReason::Upcall(data)) => { + let upcall = upcalls[data.index].clone(); + let result; if let Some(return_type) = data.return_type { result = self .function_call( &mut context, + upcall, &data.name, return_type, data.pos, @@ -1539,7 +1540,7 @@ impl Machine { .await; } else { result = - self.builtin_call(&mut context, &data.name, data.pos, data.nargs).await; + self.builtin_call(&mut context, upcall, data.pos, data.nargs).await; } match result { Ok(()) => context.pc += 1, @@ -1568,9 +1569,18 @@ impl Machine { pub async fn exec(&mut self, input: &mut dyn io::Read) -> Result { let image = compiler::compile(input, &self.symbols)?; + let upcalls = image + .upcalls + .iter() + .map(|key| match self.symbols.load(key) { + Some(Symbol::Callable(c)) => c.clone(), + _ => panic!("Builtin existence and type checking happen at compile time"), + }) + .collect::>>(); + assert!(self.data.is_empty()); self.data = image.data; - let result = self.exec_with_data(&image.instrs).await; + let result = self.exec_with_data(&upcalls, &image.instrs).await; self.data.clear(); result } From 826910535f2bdc3cc70ce1518de847f3c1e01052 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 15:27:26 -0800 Subject: [PATCH 077/110] Fix more Clippy warnings Run Clippy with --all to surface problems from the workspace that would only surface when running it from within the subcrates. --- .github/workflows/lint.sh | 2 ++ client/src/cloud.rs | 2 +- client/src/testutils.rs | 9 ++---- web/src/lib.rs | 5 +-- web/src/store.rs | 64 +++++++++++++++++++-------------------- 5 files changed, 39 insertions(+), 43 deletions(-) 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/client/src/cloud.rs b/client/src/cloud.rs index 8749e1f1..ec0209cd 100644 --- a/client/src/cloud.rs +++ b/client/src/cloud.rs @@ -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/testutils.rs b/client/src/testutils.rs index 6d2faa30..0f96e862 100644 --- a/client/src/testutils.rs +++ b/client/src/testutils.rs @@ -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)); } @@ -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 { diff --git a/web/src/lib.rs b/web/src/lib.rs index d792201c..4f05a4ba 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -281,10 +281,7 @@ impl WebTerminal { let mut machine = match builder.build() { Ok(machine) => machine, Err(e) => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Machine initialization failed: {}", e), - )) + return Err(io::Error::other(format!("Machine initialization failed: {}", e))) } }; diff --git a/web/src/store.rs b/web/src/store.rs index a2a50c43..e413bf95 100644 --- a/web/src/store.rs +++ b/web/src/store.rs @@ -158,10 +158,10 @@ 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, - format!("Failed to fetch local storage entry with index {}: {:?}", i, e), - )) + return Err(io::Error::other(format!( + "Failed to fetch local storage entry with index {}: {:?}", + i, e + ))) } }; @@ -184,25 +184,25 @@ 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, - format!("Failed to get local storage entry with key {}: {:?}", old, e), - )) + 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, - format!("Failed to put local storage entry with key {}: {:?}", new, e), - )); + 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, - format!("Failed to put remove storage entry with key {}: {:?}", old, e), - )); + return Err(io::Error::other(format!( + "Failed to put remove storage entry with key {}: {:?}", + old, e + ))); }; Ok(()) @@ -215,10 +215,10 @@ 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, - format!("Failed to get local storage entry with key {}: {:?}", key, e), - )) + return Err(io::Error::other(format!( + "Failed to get local storage entry with key {}: {:?}", + key, e + ))) } }; @@ -246,10 +246,10 @@ impl Drive for WebDrive { match self.storage.delete(key) { Ok(()) => Ok(()), - Err(e) => Err(io::Error::new( - io::ErrorKind::Other, - format!("Failed to put remove storage entry with key {}: {:?}", key, e), - )), + Err(e) => Err(io::Error::other(format!( + "Failed to put remove storage entry with key {}: {:?}", + key, e + ))), } } @@ -265,10 +265,10 @@ 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, - format!("Failed to fetch local storage entry with index {}: {:?}", i, e), - )) + return Err(io::Error::other(format!( + "Failed to fetch local storage entry with index {}: {:?}", + i, e + ))) } }; @@ -296,10 +296,10 @@ 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, - format!("Failed to put local storage entry with key {}: {:?}", key, e), - )), + Err(e) => Err(io::Error::other(format!( + "Failed to put local storage entry with key {}: {:?}", + key, e + ))), } } } From 01922a44022e6da487b0c3bae29b4cc71f44bae7 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 9 Jan 2026 16:25:15 -0800 Subject: [PATCH 078/110] Upgrade to Rust edition 2024 --- Cargo.toml | 1 + cli/Cargo.toml | 2 +- cli/src/main.rs | 4 +-- client/Cargo.toml | 2 +- client/src/cloud.rs | 6 ++-- client/src/cmds.rs | 8 ++--- client/src/testutils.rs | 32 ++++++++--------- core/Cargo.toml | 2 +- core/src/compiler/exprs.rs | 4 +-- core/src/compiler/mod.rs | 16 ++++----- core/src/lexer.rs | 2 +- core/src/parser.rs | 30 +++++++--------- core/src/syms.rs | 23 ++++++------ core/src/value.rs | 4 +-- core/tests/integration_test.rs | 6 +--- repl/Cargo.toml | 2 +- repl/src/editor.rs | 6 ++-- repl/src/lib.rs | 4 +-- rpi/Cargo.toml | 2 +- rpi/src/lib.rs | 2 +- rustfmt.toml | 2 +- sdl/Cargo.toml | 2 +- sdl/src/console.rs | 6 ++-- sdl/src/font.rs | 13 ++++--- sdl/src/host.rs | 4 +-- st7735s/Cargo.toml | 2 +- st7735s/src/lib.rs | 6 ++-- std/Cargo.toml | 2 +- std/src/console/cmds.rs | 2 +- std/src/console/drawing.rs | 2 +- std/src/console/format.rs | 6 +--- std/src/console/graphics.rs | 50 ++++++--------------------- std/src/console/mod.rs | 2 +- std/src/console/pager.rs | 2 +- std/src/console/trivial.rs | 14 ++------ std/src/exec.rs | 2 +- std/src/gfx/lcd/buffered/mod.rs | 28 ++++----------- std/src/gfx/lcd/buffered/tests.rs | 2 +- std/src/gfx/lcd/buffered/testutils.rs | 15 +++++--- std/src/gfx/lcd/fonts/font_16x16.rs | 2 +- std/src/gfx/lcd/fonts/font_5x8.rs | 2 +- std/src/gfx/lcd/mod.rs | 2 +- std/src/gfx/mod.rs | 2 +- std/src/gpio/mod.rs | 2 +- std/src/help.rs | 10 ++---- std/src/program.rs | 4 +-- std/src/storage/cmds.rs | 2 +- std/src/storage/mod.rs | 4 +-- std/src/strings.rs | 18 ++-------- std/src/testutils.rs | 2 +- std/tests/integration_test.rs | 6 +--- terminal/Cargo.toml | 2 +- terminal/src/lib.rs | 16 +++------ web/Cargo.toml | 2 +- web/src/canvas.rs | 18 +++++----- web/src/input.rs | 24 +++++-------- web/src/lib.rs | 6 ++-- web/src/store.rs | 8 ++--- 58 files changed, 181 insertions(+), 271 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 12c29593..ac3c5248 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "3" members = [ "cli", "client", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 86d9412c..31d21717 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,7 +9,7 @@ description = "The EndBASIC programming language - CLI" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" [features] default = ["crossterm"] diff --git a/cli/src/main.rs b/cli/src/main.rs index 3f16707c..8e31adf9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -22,7 +22,7 @@ #![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 +218,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/client/Cargo.toml b/client/Cargo.toml index 65d0063d..b46a737a 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,7 +9,7 @@ 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" [dependencies] async-trait = "0.1" diff --git a/client/src/cloud.rs b/client/src/cloud.rs index ec0209cd..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), - )) + )); } } } 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/testutils.rs b/client/src/testutils.rs index 0f96e862..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::*; @@ -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()); @@ -217,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 } @@ -226,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 } @@ -240,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 } @@ -256,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 } @@ -267,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..50bf2523 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,7 +9,7 @@ description = "The EndBASIC programming language - core" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" [dependencies] async-channel = "2.2" diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index a6c5caea..b8007ddc 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -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..585955b0 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -492,10 +492,10 @@ impl Compiler { 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) { @@ -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)); 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/parser.rs b/core/src/parser.rs index 96129ed6..f52420e3 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -364,7 +364,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( peeked.pos, "Expected comma, semicolon, or end of statement".to_owned(), - )) + )); } } } @@ -450,7 +450,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( token_span.pos, "Expected number after -".to_owned(), - )) + )); } } } @@ -468,7 +468,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( token_span.pos, format!("Unexpected {} in DATA statement", t), - )) + )); } } @@ -486,7 +486,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( peeked.pos, format!("Expected comma after datum but found {}", t), - )) + )); } } } @@ -531,7 +531,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 +614,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 +681,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 +964,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 @@ -1227,14 +1223,14 @@ impl<'a> Parser<'a> { 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 +1257,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( step.start_pos(), "Infinite FOR loop; STEP cannot be 0".to_owned(), - )) + )); } }; @@ -1931,7 +1927,7 @@ impl<'a> Parser<'a> { return Err(Error::Bad( token_span.pos, format!("Expected newline but found {}", token_span.token), - )) + )); } }; diff --git a/core/src/syms.rs b/core/src/syms.rs index b3c436ab..e35d1f27 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -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/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..dd85e426 100644 --- a/core/tests/integration_test.rs +++ b/core/tests/integration_test.rs @@ -71,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/repl/Cargo.toml b/repl/Cargo.toml index d196520c..1421191a 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -9,7 +9,7 @@ description = "The EndBASIC programming language - REPL" homepage = "https://www.endbasic.dev/" repository = "https://github.com/endbasic/endbasic" readme = "README.md" -edition = "2018" +edition = "2024" [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..2f6d4aa7 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -23,8 +23,8 @@ #![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..2954395f 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -9,7 +9,7 @@ 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" [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..4eb38003 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -9,7 +9,7 @@ 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" [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/st7735s/Cargo.toml b/st7735s/Cargo.toml index 9e8c7ae5..461687ea 100644 --- a/st7735s/Cargo.toml +++ b/st7735s/Cargo.toml @@ -9,7 +9,7 @@ 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" [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..e99f9074 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -9,7 +9,7 @@ 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" [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/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..a126d191 100644 --- a/std/tests/integration_test.rs +++ b/std/tests/integration_test.rs @@ -76,11 +76,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..3a92a7c3 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -9,7 +9,7 @@ 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" [dependencies] async-channel = "2.2" diff --git a/terminal/src/lib.rs b/terminal/src/lib.rs index 2bafa075..7f34dd3f 100644 --- a/terminal/src/lib.rs +++ b/terminal/src/lib.rs @@ -26,11 +26,11 @@ 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 +227,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 +427,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..cc1536d8 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -9,7 +9,7 @@ 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 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,10 +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) { - log_and_panic!("Send to unbounded channel must succeed: {}", e); - } + 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 4f05a4ba..1c6ad23a 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -23,8 +23,8 @@ #![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 +34,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,7 +281,7 @@ impl WebTerminal { let mut machine = match builder.build() { Ok(machine) => machine, Err(e) => { - return Err(io::Error::other(format!("Machine initialization failed: {}", e))) + return Err(io::Error::other(format!("Machine initialization failed: {}", e))); } }; diff --git a/web/src/store.rs b/web/src/store.rs index e413bf95..f7462655 100644 --- a/web/src/store.rs +++ b/web/src/store.rs @@ -161,7 +161,7 @@ impl WebDrive { return Err(io::Error::other(format!( "Failed to fetch local storage entry with index {}: {:?}", i, e - ))) + ))); } }; @@ -187,7 +187,7 @@ impl WebDrive { return Err(io::Error::other(format!( "Failed to get local storage entry with key {}: {:?}", old, e - ))) + ))); } }; @@ -218,7 +218,7 @@ impl WebDrive { return Err(io::Error::other(format!( "Failed to get local storage entry with key {}: {:?}", key, e - ))) + ))); } }; @@ -268,7 +268,7 @@ impl Drive for WebDrive { return Err(io::Error::other(format!( "Failed to fetch local storage entry with index {}: {:?}", i, e - ))) + ))); } }; From 67bcdad5fa4bd98e26472e5d2699f223595fad05 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 14:26:06 -0800 Subject: [PATCH 079/110] Configure lints via Cargo.toml This feature, supported since Rust 1.74, allows us to configure various lints project-wide instead of having to replicate them individually (and inconsistently) in each crate. --- Cargo.toml | 14 ++++++++++++++ cli/Cargo.toml | 3 +++ cli/build.rs | 2 ++ cli/src/main.rs | 7 ------- cli/tests/integration_test.rs | 5 ----- client/Cargo.toml | 3 +++ client/src/lib.rs | 7 ------- core/Cargo.toml | 3 +++ core/src/lib.rs | 7 ------- core/tests/integration_test.rs | 5 ----- repl/Cargo.toml | 3 +++ repl/src/lib.rs | 7 ------- rpi/Cargo.toml | 3 +++ sdl/Cargo.toml | 3 +++ sdl/src/lib.rs | 7 ------- st7735s/Cargo.toml | 3 +++ std/Cargo.toml | 3 +++ std/src/lib.rs | 7 ------- std/tests/integration_test.rs | 5 ----- terminal/Cargo.toml | 3 +++ terminal/src/lib.rs | 7 ------- web/Cargo.toml | 3 +++ web/build.rs | 17 +++++++++++++++++ web/src/lib.rs | 7 ------- 24 files changed, 63 insertions(+), 71 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac3c5248..dd828d1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,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 31d21717..c3c5b3a8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [features] default = ["crossterm"] crossterm = ["endbasic-terminal"] 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 8e31adf9..9b0c18e9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -15,13 +15,6 @@ //! 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::{Result, anyhow}; use async_channel::Sender; use endbasic_core::exec::Signal; 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 b46a737a..77f32c42 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-trait = "0.1" base64 = "0.21" 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/core/Cargo.toml b/core/Cargo.toml index 50bf2523..50d3b1cf 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-channel = "2.2" async-trait = "0.1" 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/tests/integration_test.rs b/core/tests/integration_test.rs index dd85e426..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; diff --git a/repl/Cargo.toml b/repl/Cargo.toml index 1421191a..3af843d6 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-trait = "0.1" time = { version = "0.3", features = ["std"] } diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 2f6d4aa7..3fee7a7a 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -15,13 +15,6 @@ //! 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, Console, is_narrow, refill_and_print}; use endbasic_std::program::{BREAK_MSG, Program, continue_if_modified}; diff --git a/rpi/Cargo.toml b/rpi/Cargo.toml index 2954395f..36bba0bf 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] rppal = "0.17" diff --git a/sdl/Cargo.toml b/sdl/Cargo.toml index 4eb38003..44dcd12e 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-channel = "2.2" async-trait = "0.1" 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 461687ea..446b3f28 100644 --- a/st7735s/Cargo.toml +++ b/st7735s/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-channel = "2.2" async-trait = "0.1" diff --git a/std/Cargo.toml b/std/Cargo.toml index e99f9074..b5f6d61c 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-channel = "2.2" async-trait = "0.1" 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/tests/integration_test.rs b/std/tests/integration_test.rs index a126d191..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; diff --git a/terminal/Cargo.toml b/terminal/Cargo.toml index 3a92a7c3..0bbac725 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/endbasic/endbasic" readme = "README.md" edition = "2024" +[lints] +workspace = true + [dependencies] async-channel = "2.2" async-trait = "0.1" diff --git a/terminal/src/lib.rs b/terminal/src/lib.rs index 7f34dd3f..d3fbb61a 100644 --- a/terminal/src/lib.rs +++ b/terminal/src/lib.rs @@ -15,13 +15,6 @@ //! 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}; diff --git a/web/Cargo.toml b/web/Cargo.toml index cc1536d8..83552cc5 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -13,6 +13,9 @@ 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/lib.rs b/web/src/lib.rs index 1c6ad23a..e691fc7d 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -15,13 +15,6 @@ //! 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::LineCol; use endbasic_core::exec::{Error, Result, Signal, YieldNowFn}; From 7910171d86247876018b9c5e71d984b12d539d2a Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 15:22:10 -0800 Subject: [PATCH 080/110] Simplify VarRef interface Make the struct members public and remove unnecessary accessors. --- core/src/ast.rs | 25 ++---------------- core/src/compiler/args.rs | 4 +-- core/src/compiler/exprs.rs | 4 +-- core/src/compiler/mod.rs | 53 +++++++++++++++++++------------------- core/src/parser.rs | 12 ++++----- core/src/syms.rs | 14 +++++----- 6 files changed, 45 insertions(+), 67 deletions(-) diff --git a/core/src/ast.rs b/core/src/ast.rs index 26b8419b..b2c61a2d 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -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 { 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 b8007ddc..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) diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 585955b0..12d28787 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -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,7 +489,7 @@ 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() @@ -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,7 +516,7 @@ impl Compiler { return Err(Error::IncompatibleTypesInAssignment(vref_pos, etype, vtype)); } - if let Some(ref_type) = vref.ref_type() + if let Some(ref_type) = vref.ref_type && ref_type != vtype { return Err(Error::IncompatibleTypeAnnotationInReference(vref_pos, vref)); @@ -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)); } diff --git a/core/src/parser.rs b/core/src/parser.rs index f52420e3..d6cbbb8a 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 @@ -1217,7 +1217,7 @@ 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( @@ -1422,8 +1422,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 } @@ -1450,7 +1450,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(), diff --git a/core/src/syms.rs b/core/src/syms.rs index e35d1f27..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,7 +457,7 @@ impl Symbols { } Some(_) => Err(value::Error::new(format!("Cannot redefine {} as a variable", vref))), None => { - if let Some(ref_type) = vref.ref_type() + if let Some(ref_type) = vref.ref_type && !vref.accepts(value.as_exprtype()) { return Err(value::Error::new(format!( From 42812ffd0aefc5221cbff42c17b5e9ae0c8bb615 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 19:13:00 -0800 Subject: [PATCH 081/110] Track data sequences as expressions, not values This helps decouple the AST from Value types and, because Exprs carry a position that needs to be checked for in tests, highlights that there was a subtle bug in position reporting for negative data numeric values. --- core/src/ast.rs | 2 +- core/src/compiler/mod.rs | 21 +++++++++++-- core/src/parser.rs | 65 ++++++++++++++++++++++++++-------------- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/core/src/ast.rs b/core/src/ast.rs index b2c61a2d..c5a34568 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -496,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/mod.rs b/core/src/compiler/mod.rs index 12d28787..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, @@ -1239,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/parser.rs b/core/src/parser.rs index d6cbbb8a..d72a6e4d 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -436,16 +436,30 @@ 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, @@ -2373,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) }))], + }), ], ); @@ -2382,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) })), ], })], ); @@ -2395,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, ], })], @@ -2410,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) })), + ], })], ); } @@ -2421,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] From addc75e05ed3fa2b6478ab46405d5b5a669a9112 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 12 Jan 2026 18:23:21 -0800 Subject: [PATCH 082/110] Remove SymbolKey from the AST Make VarRefs carry the original symbol name as specified in the code and convert it to a SymbolKey when necessary. This preserves the case of the symbols for error message reporting and helps free the AST from the SymbolKey type. --- core/src/ast.rs | 4 ++-- core/src/exec.rs | 50 +++++++++++++++++++------------------------ core/src/testutils.rs | 2 +- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/core/src/ast.rs b/core/src/ast.rs index c5a34568..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. @@ -311,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 { 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/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(()) } } From 0262523ec740d45c51e04d4ca4659fe26de41fcb Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 26 Jan 2026 22:25:01 -0800 Subject: [PATCH 083/110] Fix DIM docs to not mention type annotations Variable names passed to DIM cannot currently carry a type annotation. Maybe they should be able to, but for now reflect reality. --- cli/tests/repl/help.out | 1 - core/src/parser.rs | 2 ++ std/src/lang.md | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/tests/repl/help.out b/cli/tests/repl/help.out index 9a7248ad..1d8a2a3c 100644 --- a/cli/tests/repl/help.out +++ b/cli/tests/repl/help.out @@ -605,7 +605,6 @@ Output from HELP "VARIABLES": The following are all equivalent: DIM foo AS BOOLEAN - DIM foo? AS BOOLEAN foo? = FALSE Arrays must be defined with the `DIM` keyword before they can be used. diff --git a/core/src/parser.rs b/core/src/parser.rs index d72a6e4d..76a64590 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -548,6 +548,8 @@ impl<'a> Parser<'a> { )); } }; + // TODO(jmmv): Why do we require unannotated strings? We could also take one and then + // skip the `AS ` portion. let name = vref_to_unannotated_string(vref, token_span.pos)?; let name_pos = token_span.pos; diff --git a/std/src/lang.md b/std/src/lang.md index 84d6337b..95db8d73 100644 --- a/std/src/lang.md +++ b/std/src/lang.md @@ -58,7 +58,6 @@ Variable identifiers are alphanumeric words that start with a letter or special Variables can be first defined either via an assignment or via the `DIM` keyword, the latter of which sets the variable to its zero value. The following are all equivalent: DIM foo AS BOOLEAN - DIM foo? AS BOOLEAN foo? = FALSE Arrays must be defined with the `DIM` keyword before they can be used. The following example defines a matrix and sets the value of a single element within it: From a35fa941575b73c8b39be4d4bb3b0f53c62901f3 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 21 Feb 2026 05:52:55 -0800 Subject: [PATCH 084/110] Refactor GPIO mocks to be self-contained Replace the old MockPins implementation, which borrowed the machine's symbol table at call time and required users to set up __GPIO_MOCK_DATA and __GPIO_MOCK_LAST variables, with a self-contained MockPins that maintains its own reads queue and operation trace. Add a GPIO_MOCK_INJECT command and a GPIO_MOCK_TRACE function (registered only with --gpio-pins=mock) to pre-seed reads and dump the trace, and add a --gpio-pins CLI flag to select between rppal, noop, and mock backends. Rewrite the gpio.bas integration test to use these commands instead of the old magic variables. Update all unit tests to use the new make_mock_machine helper directly. --- cli/src/main.rs | 87 +++++++++--- cli/tests/cli/help.out | 6 + cli/tests/cli/help.out.rpi | 7 + cli/tests/cli/help.out.rpi.sdl | 7 + cli/tests/cli/help.out.sdl | 6 + cli/tests/examples/gpio.bas | 12 +- cli/tests/examples/gpio.out | 89 +++++------- cli/tests/integration_test.rs | 7 +- core/src/syms.rs | 20 +-- rpi/src/gpio.rs | 9 ++ std/src/gpio/fakes.rs | 217 ++++++++-------------------- std/src/gpio/mod.rs | 252 ++++++++++++++++++++++----------- std/src/testutils.rs | 8 +- 13 files changed, 383 insertions(+), 344 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 9b0c18e9..09b4ff78 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -19,6 +19,7 @@ use anyhow::{Result, anyhow}; use async_channel::Sender; use endbasic_core::exec::Signal; use endbasic_std::console::{Console, ConsoleSpec}; +use endbasic_std::gpio; use endbasic_std::storage::Storage; use getoptsargs::prelude::*; use std::cell::RefCell; @@ -62,31 +63,49 @@ fn app_extra_help(o: &mut dyn io::Write) -> io::Result<()> { writeln!(o, " fg_color=COLOR,bg_color=COLOR,font=NAME")?; } writeln!(o, " text enables the text-based console")?; + writeln!(o)?; + writeln!(o, "GPIO-PINS-SPEC can be one of the following:")?; + writeln!(o, " mock mock backend for testing")?; + writeln!(o, " noop dummy backend that always returns errors")?; + if cfg!(feature = "rpi") { + writeln!(o, " rppal uses the Raspberry Pi GPIO hardware")?; + } Ok(()) } -/// Creates a new EndBASIC machine builder based on the features enabled in this crate. -fn new_machine_builder(console_spec: Option<&str>) -> io::Result { - /// Obtains the default set of pins for a Raspberry Pi. - #[cfg(feature = "rpi")] - fn add_gpio_pins(builder: endbasic_std::MachineBuilder) -> endbasic_std::MachineBuilder { - // TODO(jmmv): If st7735s is in use, this basically creates a secondary set of pins. - // Which... should work OK, but then the user can interfere with the display in ways that - // should not be allowed, probably. - builder.with_gpio_pins(Rc::from(RefCell::from(endbasic_rpi::RppalPins::default()))) - } +/// Creates the rppal GPIO backend when the rpi feature is compiled in. +#[cfg(feature = "rpi")] +fn setup_gpio_pins_rppal() -> Result>> { + Ok(Rc::new(RefCell::new(endbasic_rpi::RppalPins::default()))) +} - /// Obtains the default set of pins for a platform without GPIO support. - #[cfg(not(feature = "rpi"))] - fn add_gpio_pins(builder: endbasic_std::MachineBuilder) -> endbasic_std::MachineBuilder { - builder +/// Errors out for rppal when the rpi feature is not compiled in. +#[cfg(not(feature = "rpi"))] +fn setup_gpio_pins_rppal() -> Result>> { + Err(UsageError::new("--gpio-pins=rppal requires the rpi feature to be compiled in").into()) +} + +/// Parses the `--gpio-pins` flag value and constructs the pins backend. +fn setup_gpio_pins(spec: Option<&str>) -> Result>> { + let spec = if cfg!(feature = "rpi") { spec.unwrap_or("rppal") } else { spec.unwrap_or("noop") }; + match spec { + "mock" => Ok(Rc::new(RefCell::new(gpio::MockPins::default()))), + "noop" => Ok(Rc::new(RefCell::new(gpio::NoopPins::default()))), + "rppal" => setup_gpio_pins_rppal(), + other => Err(UsageError::new(format!("Unknown --gpio-pins backend: {}", other)).into()), } +} +/// Creates a new EndBASIC machine builder based on the features enabled in this crate. +fn new_machine_builder( + console_spec: Option<&str>, + gpio_pins_spec: Option<&str>, +) -> Result { let signals_chan = async_channel::unbounded(); let mut builder = endbasic_std::MachineBuilder::default(); builder = builder.with_console(setup_console(console_spec, signals_chan.0.clone())?); builder = builder.with_signals_chan(signals_chan); - builder = add_gpio_pins(builder); + builder = builder.with_gpio_pins(setup_gpio_pins(gpio_pins_spec)?); Ok(builder) } @@ -242,10 +261,11 @@ pub fn setup_storage(storage: &mut Storage, local_drive_spec: &str) -> io::Resul /// `service_url` is the base URL of the cloud service. async fn run_repl_loop( console_spec: Option<&str>, + gpio_pins_spec: Option<&str>, local_drive_spec: &str, service_url: &str, ) -> Result { - let mut builder = make_interactive(new_machine_builder(console_spec)?); + let mut builder = make_interactive(new_machine_builder(console_spec, gpio_pins_spec)?); let console = builder.get_console(); let program = builder.get_program(); @@ -260,8 +280,13 @@ async fn run_repl_loop( } /// Executes the `path` program in a fresh machine. -async fn run_script>(path: P, console_spec: Option<&str>) -> Result { - let mut machine = new_machine_builder(console_spec)?.build()?; +async fn run_script>( + path: P, + console_spec: Option<&str>, + gpio_pins_spec: Option<&str>, +) -> Result { + let builder = new_machine_builder(console_spec, gpio_pins_spec)?; + let mut machine = builder.build()?; let mut input = File::open(path)?; Ok(machine.exec(&mut input).await?.as_exit_code()) } @@ -277,10 +302,11 @@ async fn run_script>(path: P, console_spec: Option<&str>) -> Resu async fn run_interactive( path: &str, console_spec: Option<&str>, + gpio_pins_spec: Option<&str>, local_drive_spec: &str, service_url: &str, ) -> Result { - let mut builder = make_interactive(new_machine_builder(console_spec)?); + let mut builder = make_interactive(new_machine_builder(console_spec, gpio_pins_spec)?); let console = builder.get_console(); let program = builder.get_program(); @@ -317,6 +343,7 @@ fn app_build(builder: Builder) -> Builder { .homepage("https://www.endbasic.dev/") .bugs("https://github.com/endbasic/endbasic/issues") .optopt("", "console", "type and properties of the console to use", "CONSOLE-SPEC") + .optopt("", "gpio-pins", "GPIO pins backend to use", "GPIO-PINS-SPEC") .optflag("i", "interactive", "force interactive mode when running a script") .optopt("", "local-drive", "location of the drive to mount as LOCAL", "URI") .optopt("", "service-url", "base URL of the cloud service", "URL") @@ -327,6 +354,8 @@ fn app_build(builder: Builder) -> Builder { async fn app_main(matches: Matches) -> Result { let console_spec = matches.opt_str("console"); + let gpio_pins_spec = matches.opt_str("gpio-pins"); + let service_url = matches .opt_str("service-url") .unwrap_or_else(|| endbasic_client::PROD_API_ADDRESS.to_owned()); @@ -334,15 +363,27 @@ async fn app_main(matches: Matches) -> Result { match matches.arg_trail() { [] => { let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?; - Ok(run_repl_loop(console_spec.as_deref(), &local_drive, &service_url).await?) + Ok(run_repl_loop( + console_spec.as_deref(), + gpio_pins_spec.as_deref(), + &local_drive, + &service_url, + ) + .await?) } [file] => { if matches.opt_present("interactive") { let local_drive = get_local_drive_spec(matches.opt_str("local-drive"))?; - Ok(run_interactive(file, console_spec.as_deref(), &local_drive, &service_url) - .await?) + Ok(run_interactive( + file, + console_spec.as_deref(), + gpio_pins_spec.as_deref(), + &local_drive, + &service_url, + ) + .await?) } else { - Ok(run_script(file, console_spec.as_deref()).await?) + Ok(run_script(file, console_spec.as_deref(), gpio_pins_spec.as_deref()).await?) } } [_, ..] => Err(UsageError::new("Too many arguments").into()), diff --git a/cli/tests/cli/help.out b/cli/tests/cli/help.out index c3fe3c3a..3e778fad 100644 --- a/cli/tests/cli/help.out +++ b/cli/tests/cli/help.out @@ -5,6 +5,8 @@ Options: --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use + --gpio-pins GPIO-PINS-SPEC + GPIO pins backend to use -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL @@ -17,5 +19,9 @@ Arguments: CONSOLE-SPEC can be one of the following: text enables the text-based console +GPIO-PINS-SPEC can be one of the following: + mock mock backend for testing + noop dummy backend that always returns errors + Report bugs to: https://github.com/endbasic/endbasic/issues EndBASIC home page: https://www.endbasic.dev/ diff --git a/cli/tests/cli/help.out.rpi b/cli/tests/cli/help.out.rpi index 32083c78..9adb9686 100644 --- a/cli/tests/cli/help.out.rpi +++ b/cli/tests/cli/help.out.rpi @@ -5,6 +5,8 @@ Options: --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use + --gpio-pins GPIO-PINS-SPEC + GPIO pins backend to use -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL @@ -18,5 +20,10 @@ CONSOLE-SPEC can be one of the following: st7735s enables the ST7735S LCD console text enables the text-based console +GPIO-PINS-SPEC can be one of the following: + mock mock backend for testing + noop dummy backend that always returns errors + rppal uses the Raspberry Pi GPIO hardware + Report bugs to: https://github.com/endbasic/endbasic/issues EndBASIC home page: https://www.endbasic.dev/ diff --git a/cli/tests/cli/help.out.rpi.sdl b/cli/tests/cli/help.out.rpi.sdl index 2b4c8de4..8145ab80 100644 --- a/cli/tests/cli/help.out.rpi.sdl +++ b/cli/tests/cli/help.out.rpi.sdl @@ -5,6 +5,8 @@ Options: --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use + --gpio-pins GPIO-PINS-SPEC + GPIO pins backend to use -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL @@ -27,5 +29,10 @@ CONSOLE-SPEC can be one of the following: fg_color=COLOR,bg_color=COLOR,font=NAME text enables the text-based console +GPIO-PINS-SPEC can be one of the following: + mock mock backend for testing + noop dummy backend that always returns errors + rppal uses the Raspberry Pi GPIO hardware + Report bugs to: https://github.com/endbasic/endbasic/issues EndBASIC home page: https://www.endbasic.dev/ diff --git a/cli/tests/cli/help.out.sdl b/cli/tests/cli/help.out.sdl index 6f1c585b..fc33ec22 100644 --- a/cli/tests/cli/help.out.sdl +++ b/cli/tests/cli/help.out.sdl @@ -5,6 +5,8 @@ Options: --version show version information and exit --console CONSOLE-SPEC type and properties of the console to use + --gpio-pins GPIO-PINS-SPEC + GPIO pins backend to use -i, --interactive force interactive mode when running a script --local-drive URI location of the drive to mount as LOCAL @@ -24,5 +26,9 @@ CONSOLE-SPEC can be one of the following: 'WIDTHxHEIGHT' or 'WIDTHxHEIGHTfs' text enables the text-based console +GPIO-PINS-SPEC can be one of the following: + mock mock backend for testing + noop dummy backend that always returns errors + Report bugs to: https://github.com/endbasic/endbasic/issues EndBASIC home page: https://www.endbasic.dev/ diff --git a/cli/tests/examples/gpio.bas b/cli/tests/examples/gpio.bas index e80400d7..91dbf212 100644 --- a/cli/tests/examples/gpio.bas +++ b/cli/tests/examples/gpio.bas @@ -17,17 +17,13 @@ LOAD "DEMOS:/GPIO.BAS" -DIM __GPIO_MOCK_DATA(20) AS INTEGER -__GPIO_MOCK_DATA(3) = 811 ' Return high for pin 8 (button). -__GPIO_MOCK_DATA(4) = 811 ' Return high for pin 8 (button). -__GPIO_MOCK_DATA(5) = 810 ' Return low for pin 8 (button). -__GPIO_MOCK_LAST = 0 +GPIO_MOCK_INJECT 8, TRUE +GPIO_MOCK_INJECT 8, TRUE +GPIO_MOCK_INJECT 8, FALSE RUN PRINT "Dumping GPIO trace..." -FOR i = 0 TO __GPIO_MOCK_LAST - 1 - PRINT "__GPIO_MOCK_DATA", i, __GPIO_MOCK_DATA(i) -NEXT +PRINT GPIO_MOCK_TRACE$ DISASM diff --git a/cli/tests/examples/gpio.out b/cli/tests/examples/gpio.out index eb91622f..ade18097 100644 --- a/cli/tests/examples/gpio.out +++ b/cli/tests/examples/gpio.out @@ -30,22 +30,7 @@ Button pressed! Blinking LED on pin 18 ... 2 1 Dumping GPIO trace... -__GPIO_MOCK_DATA 0 -1 -__GPIO_MOCK_DATA 1 803 -__GPIO_MOCK_DATA 2 1804 -__GPIO_MOCK_DATA 3 811 -__GPIO_MOCK_DATA 4 811 -__GPIO_MOCK_DATA 5 810 -__GPIO_MOCK_DATA 6 1821 -__GPIO_MOCK_DATA 7 1820 -__GPIO_MOCK_DATA 8 1821 -__GPIO_MOCK_DATA 9 1820 -__GPIO_MOCK_DATA 10 1821 -__GPIO_MOCK_DATA 11 1820 -__GPIO_MOCK_DATA 12 1821 -__GPIO_MOCK_DATA 13 1820 -__GPIO_MOCK_DATA 14 1821 -__GPIO_MOCK_DATA 15 1820 +-1 -1 803 1804 811 811 810 1821 1820 1821 1820 1821 1820 1821 1820 1821 1820 0000 PUSH% 8 # 16:10 0001 SETV BUTTON 0002 PUSH% 18 # 17:7 @@ -53,29 +38,29 @@ __GPIO_MOCK_DATA 15 1820 0004 CALLB 6 (CLS), 0 # 19:1 0005 PUSH% 11 # 20:7 0006 CALLB 7 (COLOR), 1 # 20:1 -0007 CALLB 48 (PRINT), 0 # 21:1 +0007 CALLB 50 (PRINT), 0 # 21:1 0008 PUSH$ " GPIO demo" # 22:7 0009 PUSH% 4 # 22:7 -000a CALLB 48 (PRINT), 2 # 22:1 +000a CALLB 50 (PRINT), 2 # 22:1 000b PUSH$ "===========" # 23:7 000c PUSH% 4 # 23:7 -000d CALLB 48 (PRINT), 2 # 23:1 +000d CALLB 50 (PRINT), 2 # 23:1 000e CALLB 7 (COLOR), 0 # 24:1 -000f CALLB 48 (PRINT), 0 # 25:1 +000f CALLB 50 (PRINT), 0 # 25:1 0010 PUSH$ "This demo showcases how to poll a hardware button attached to a GPIO" # 26:7 0011 PUSH% 4 # 26:7 -0012 CALLB 48 (PRINT), 2 # 26:1 +0012 CALLB 50 (PRINT), 2 # 26:1 0013 PUSH$ "pin and how to flash an LED attached to another one." # 27:7 0014 PUSH% 4 # 27:7 -0015 CALLB 48 (PRINT), 2 # 27:1 -0016 CALLB 48 (PRINT), 0 # 28:1 +0015 CALLB 50 (PRINT), 2 # 27:1 +0016 CALLB 50 (PRINT), 0 # 28:1 0017 PUSH% 2 # 29:7 0018 CALLB 7 (COLOR), 1 # 29:1 0019 PUSH$ "To get started, follow these steps:" # 30:7 001a PUSH% 4 # 30:7 -001b CALLB 48 (PRINT), 2 # 30:1 +001b CALLB 50 (PRINT), 2 # 30:1 001c CALLB 7 (COLOR), 0 # 31:1 -001d CALLB 48 (PRINT), 0 # 32:1 +001d CALLB 50 (PRINT), 0 # 32:1 001e PUSH$ "and to ground; don't forget to add a" # 33:40 001f PUSH% 4 # 33:40 0020 PUSH% 1 # 33:38 @@ -84,11 +69,11 @@ __GPIO_MOCK_DATA 15 1820 0023 PUSH% 1 # 33:33 0024 PUSH$ "1. Connect an LED to pin" # 33:7 0025 PUSH% 4 # 33:7 -0026 CALLB 48 (PRINT), 8 # 33:1 +0026 CALLB 50 (PRINT), 8 # 33:1 0027 PUSH$ " resistor inline." # 34:7 0028 PUSH% 4 # 34:7 -0029 CALLB 48 (PRINT), 2 # 34:1 -002a CALLB 48 (PRINT), 0 # 35:1 +0029 CALLB 50 (PRINT), 2 # 34:1 +002a CALLB 50 (PRINT), 0 # 35:1 002b PUSH$ "and to ground. We'll be" # 36:50 002c PUSH% 4 # 36:50 002d PUSH% 1 # 36:48 @@ -97,50 +82,50 @@ __GPIO_MOCK_DATA 15 1820 0030 PUSH% 1 # 36:40 0031 PUSH$ "2. Connect a push button to pin" # 36:7 0032 PUSH% 4 # 36:7 -0033 CALLB 48 (PRINT), 8 # 36:1 +0033 CALLB 50 (PRINT), 8 # 36:1 0034 PUSH$ " using the built-in pull-up resistor for the input pin so there is" # 37:7 0035 PUSH% 4 # 37:7 -0036 CALLB 48 (PRINT), 2 # 37:1 +0036 CALLB 50 (PRINT), 2 # 37:1 0037 PUSH$ " no need to do any extra wiring." # 38:7 0038 PUSH% 4 # 38:7 -0039 CALLB 48 (PRINT), 2 # 38:1 -003a CALLB 48 (PRINT), 0 # 39:1 +0039 CALLB 50 (PRINT), 2 # 38:1 +003a CALLB 50 (PRINT), 0 # 39:1 003b PUSH% 1 # 40:7 003c CALLB 7 (COLOR), 1 # 40:1 003d PUSH$ "This demo is only functional on the Raspberry Pi and assumes you have" # 41:7 003e PUSH% 4 # 41:7 -003f CALLB 48 (PRINT), 2 # 41:1 +003f CALLB 50 (PRINT), 2 # 41:1 0040 PUSH$ "built EndBASIC with --features=rpi. If these conditions are not met," # 42:7 0041 PUSH% 4 # 42:7 -0042 CALLB 48 (PRINT), 2 # 42:1 +0042 CALLB 50 (PRINT), 2 # 42:1 0043 PUSH$ "the demo will fail to run." # 43:7 0044 PUSH% 4 # 43:7 -0045 CALLB 48 (PRINT), 2 # 43:1 +0045 CALLB 50 (PRINT), 2 # 43:1 0046 CALLB 7 (COLOR), 0 # 44:1 -0047 CALLB 48 (PRINT), 0 # 45:1 +0047 CALLB 50 (PRINT), 0 # 45:1 0048 LOADR DUMMY # 46:71 0049 PUSH% 2 # 46:69 004a PUSH$ "Press ENTER when you are ready or CTRL+C to exit the demo..." # 46:7 004b PUSH% 1 # 46:7 -004c CALLB 30 (INPUT), 4 # 46:1 +004c CALLB 32 (INPUT), 4 # 46:1 004d CALLB 6 (CLS), 0 # 48:1 004e PUSH% 11 # 49:7 004f CALLB 7 (COLOR), 1 # 49:1 -0050 CALLB 48 (PRINT), 0 # 50:1 +0050 CALLB 50 (PRINT), 0 # 50:1 0051 PUSH$ " GPIO demo" # 51:7 0052 PUSH% 4 # 51:7 -0053 CALLB 48 (PRINT), 2 # 51:1 +0053 CALLB 50 (PRINT), 2 # 51:1 0054 PUSH$ "===========" # 52:7 0055 PUSH% 4 # 52:7 -0056 CALLB 48 (PRINT), 2 # 52:1 +0056 CALLB 50 (PRINT), 2 # 52:1 0057 CALLB 7 (COLOR), 0 # 53:1 -0058 CALLB 48 (PRINT), 0 # 54:1 +0058 CALLB 50 (PRINT), 0 # 54:1 0059 PUSH$ "IN-PULL-UP" # 57:20 005a LOAD% BUTTON # 57:12 -005b CALLB 26 (GPIO_SETUP), 2 # 57:1 +005b CALLB 28 (GPIO_SETUP), 2 # 57:1 005c PUSH$ "OUT" # 58:17 005d LOAD% LED # 58:12 -005e CALLB 26 (GPIO_SETUP), 2 # 58:1 +005e CALLB 28 (GPIO_SETUP), 2 # 58:1 005f PUSH$ "..." # 62:52 0060 PUSH% 4 # 62:52 0061 PUSH% 1 # 62:50 @@ -149,12 +134,12 @@ __GPIO_MOCK_DATA 15 1820 0064 PUSH% 1 # 62:42 0065 PUSH$ "Waiting for a button press on pin" # 62:7 0066 PUSH% 4 # 62:7 -0067 CALLB 48 (PRINT), 8 # 62:1 +0067 CALLB 50 (PRINT), 8 # 62:1 0068 LOAD% BUTTON # 63:17 -0069 CALLF? 25 (GPIO_READ), 1 # 63:7 +0069 CALLF? 27 (GPIO_READ), 1 # 63:7 006a JMPNT 006e 006b PUSH# 0.05 # 64:11 -006c CALLB 64 (SLEEP), 1 # 64:5 +006c CALLB 66 (SLEEP), 1 # 64:5 006d JMP 0068 006e PUSH$ "..." # 68:51 006f PUSH% 4 # 68:51 @@ -164,7 +149,7 @@ __GPIO_MOCK_DATA 15 1820 0073 PUSH% 1 # 68:44 0074 PUSH$ "Button pressed! Blinking LED on pin" # 68:7 0075 PUSH% 4 # 68:7 -0076 CALLB 48 (PRINT), 8 # 68:1 +0076 CALLB 50 (PRINT), 8 # 68:1 0077 PUSH% 5 # 69:9 0078 SETV I 0079 LOAD% I # 69:5 @@ -173,17 +158,17 @@ __GPIO_MOCK_DATA 15 1820 007c JMPNT 008f 007d PUSH? true # 70:21 007e LOAD% LED # 70:16 -007f CALLB 27 (GPIO_WRITE), 2 # 70:5 +007f CALLB 29 (GPIO_WRITE), 2 # 70:5 0080 PUSH# 0.1 # 71:11 -0081 CALLB 64 (SLEEP), 1 # 71:5 +0081 CALLB 66 (SLEEP), 1 # 71:5 0082 PUSH? false # 72:21 0083 LOAD% LED # 72:16 -0084 CALLB 27 (GPIO_WRITE), 2 # 72:5 +0084 CALLB 29 (GPIO_WRITE), 2 # 72:5 0085 PUSH# 0.1 # 73:11 -0086 CALLB 64 (SLEEP), 1 # 73:5 +0086 CALLB 66 (SLEEP), 1 # 73:5 0087 LOAD% I # 74:11 0088 PUSH% 3 # 74:11 -0089 CALLB 48 (PRINT), 2 # 74:5 +0089 CALLB 50 (PRINT), 2 # 74:5 008a LOAD% I # 69:5 008b PUSH% -1 # 69:22 008c ADD% # 69:11 diff --git a/cli/tests/integration_test.rs b/cli/tests/integration_test.rs index c5e239ac..984abb19 100644 --- a/cli/tests/integration_test.rs +++ b/cli/tests/integration_test.rs @@ -360,7 +360,12 @@ fn test_example_fibonacci() { fn test_example_gpio() { check( bin_path("endbasic"), - &["--local-drive=memory://", "--interactive", &src_str("cli/tests/examples/gpio.bas")], + &[ + "--gpio-pins=mock", + "--local-drive=memory://", + "--interactive", + &src_str("cli/tests/examples/gpio.bas"), + ], 0, Behavior::File(src_path("cli/tests/examples/gpio.in")), Behavior::File(src_path("cli/tests/examples/gpio.out")), diff --git a/core/src/syms.rs b/core/src/syms.rs index 51c6d1c1..257800e5 100644 --- a/core/src/syms.rs +++ b/core/src/syms.rs @@ -274,13 +274,7 @@ impl Symbols { pub fn clear(&mut self) { fn filter(key: &SymbolKey, symbol: &mut Symbol) -> bool { let is_internal = key.0.starts_with(|c: char| c.is_ascii_digit()); - - // TODO(jmmv): Preserving symbols that start with __ is a hack that was added to support - // the already-existing GPIO tests when RUN was changed to issue a CLEAR upfront. This - // is undocumented behavior and we should find a nicer way to do this. - let is_gpio_hack = key.0.starts_with("__"); - - is_internal || is_gpio_hack || !symbol.user_defined() + is_internal || !symbol.user_defined() } self.globals.retain(filter); @@ -832,26 +826,26 @@ mod tests { .add_callable(OutCommand::new(Rc::from(RefCell::from(vec![])))) .add_callable(SumFunction::new()) .add_var("SOMEVAR", Value::Boolean(true)) - .add_var("__SYSTEM_VAR", Value::Integer(42)) + .add_var("__OLD_STYLE_PRIVATE_VAR", Value::Integer(42)) .add_global_var("GLOBAL_VAR", Value::Integer(43)) - .add_global_var("__GLOBAL_SYSTEM_VAR", Value::Integer(44)) + .add_global_var("__GLOBAL_OLD_STYLE_PRIVATE_VAR", Value::Integer(44)) .build(); assert!(syms.get(&VarRef::new("SOMEARRAY", None)).unwrap().is_some()); assert!(syms.get(&VarRef::new("OUT", None)).unwrap().is_some()); assert!(syms.get(&VarRef::new("SUM", None)).unwrap().is_some()); assert!(syms.get(&VarRef::new("SOMEVAR", None)).unwrap().is_some()); - assert!(syms.get(&VarRef::new("__SYSTEM_VAR", None)).unwrap().is_some()); + assert!(syms.get(&VarRef::new("__OLD_STYLE_PRIVATE_VAR", None)).unwrap().is_some()); assert!(syms.get(&VarRef::new("GLOBAL_VAR", None)).unwrap().is_some()); - assert!(syms.get(&VarRef::new("__GLOBAL_SYSTEM_VAR", None)).unwrap().is_some()); + assert!(syms.get(&VarRef::new("__GLOBAL_OLD_STYLE_PRIVATE_VAR", None)).unwrap().is_some()); syms.clear(); assert!(syms.get(&VarRef::new("SOMEARRAY", None)).unwrap().is_none()); assert!(syms.get(&VarRef::new("OUT", None)).unwrap().is_some()); assert!(syms.get(&VarRef::new("SUM", None)).unwrap().is_some()); assert!(syms.get(&VarRef::new("SOMEVAR", None)).unwrap().is_none()); - assert!(syms.get(&VarRef::new("__SYSTEM_VAR", None)).unwrap().is_some()); + assert!(syms.get(&VarRef::new("__OLD_STYLE_PRIVATE_VAR", None)).unwrap().is_none()); assert!(syms.get(&VarRef::new("GLOBAL_VAR", None)).unwrap().is_none()); - assert!(syms.get(&VarRef::new("__GLOBAL_SYSTEM_VAR", None)).unwrap().is_some()); + assert!(syms.get(&VarRef::new("__GLOBAL_OLD_STYLE_PRIVATE_VAR", None)).unwrap().is_none()); } #[test] diff --git a/rpi/src/gpio.rs b/rpi/src/gpio.rs index 32f0333e..d0836dea 100644 --- a/rpi/src/gpio.rs +++ b/rpi/src/gpio.rs @@ -17,6 +17,7 @@ use endbasic_std::gpio::{Pin, PinMode, Pins}; use rppal::gpio; +use std::any::Any; use std::collections::HashMap; use std::io; @@ -54,6 +55,14 @@ impl RppalPins { } impl Pins for RppalPins { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()> { self.clear(pin)?; let chip = self.get_chip()?; diff --git a/std/src/gpio/fakes.rs b/std/src/gpio/fakes.rs index 8ab30402..6b6f5c41 100644 --- a/std/src/gpio/fakes.rs +++ b/std/src/gpio/fakes.rs @@ -16,15 +16,23 @@ //! Fake implementations of GPIO pins that work on all platforms. use crate::gpio::{Pin, PinMode, Pins}; -use endbasic_core::ast::{ExprType, Value, VarRef}; -use endbasic_core::syms::{Array, Symbol, Symbols}; +use std::any::Any; +use std::collections::VecDeque; use std::io; /// Stand-in implementation of the EndBASIC GPIO operations that always returns an error. #[derive(Default)] -pub(crate) struct NoopPins {} +pub struct NoopPins {} impl Pins for NoopPins { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn setup(&mut self, _pin: Pin, _mode: PinMode) -> io::Result<()> { Err(io::Error::other("GPIO backend not compiled in")) } @@ -46,37 +54,6 @@ impl Pins for NoopPins { } } -/// Mock GPIO implementation that tracks operations and supplies fake reads. -/// -/// This is an undocumented feature to support unit-testing of our own demo code and is quite -/// convoluted due to the fact that we don't have a lot of freedom in what we can do in the context -/// of the `Pins` implementation: we get no context passed in (intentionally), so the only context -/// we can grab with some contortions is a reference to the machine's symbols. This reference is -/// only valid for the duration of a GPIO call and thus this structure must be recreated on each -/// GPIO call and cannot maintain state of its own (other than via symbols). -/// -/// To enable mocking, the user must define the `__GPIO_MOCK_DATA` unidimensional array of integer -/// type (aka `data`) and the `__GPIO_MOCK_LAST` integer variable (aka `last`). If these types are -/// not met, mocking is silently not enabled. -/// -/// The `data` array represents an ordered trace of GPIO calls and `last` indicates the index -/// within the array that should be inspected on the next GPIO call. Each `data[last]` value is -/// encoded as a pin number (times 100) plus a `MockOp` integer that identifies the operation that -/// happened. -/// -/// For mutating GPIO calls, `data[last]` has to be zero on entry (or else the operation will fail) -/// and will be updated with the affected pin number and the operation. The meta operation to clear -/// all pins has a special number. -/// -/// For read GPIO calls, `data[last]` has to contain the pin number that matches the read operation -/// and the desired outcome of the operation. -/// -/// When a test is complete, the test should inspect the values in `data` up to the `last` position -/// and ensure they match expectations. -pub(crate) struct MockPins<'a> { - symbols: &'a mut Symbols, -} - /// Per-pin operation identifier in the mock data. #[derive(PartialEq)] enum MockOp { @@ -96,125 +73,49 @@ enum MockOp { } impl MockOp { - /// Encodes a `pin` and `op` pair as a datum for the mock data. + /// Encodes a `pin` and `op` pair as a trace datum. fn encode(pin: Pin, op: Self) -> i32 { assert!(op != Self::ClearAll); (pin.0 as i32) * 100 + (op as i32) } - - /// Decodes a datum from the mock data that is to be used for a read operation. - fn decode_read(pos: i32, datum: i32) -> io::Result<(Pin, bool)> { - if datum < 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Negative read value at __GPIO_MOCK_DATA({})", pos), - )); - } - let pin = datum / 100; - if pin > u8::MAX as i32 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Pin number too large at __GPIO_MOCK_DATA({})", pos), - )); - } - let pin = Pin(pin as u8); - match datum % 100 { - i if i == (MockOp::ReadLow as i32) => Ok((pin, false)), - i if i == (MockOp::ReadHigh as i32) => Ok((pin, true)), - i => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unknown read operation {} at __GPIO_MOCK_DATA({})", i, pos), - )), - } - } } -impl<'a> MockPins<'a> { - /// Creates a mock pins instance if `__GPIO_MOCK_DATA` *and* `__GPIO_MOCK_LAST` are set and of - /// the correct types. - /// - /// Note that, while this object is alive (which is in the context of an individual GPIO call), - /// we know that these symbols are valid so other methods can assume that they are. - pub(crate) fn try_new(symbols: &'a mut Symbols) -> Option> { - if MockPins::get_last(symbols).is_none() || MockPins::get_mut_data(symbols).is_none() { - None - } else { - Some(Self { symbols }) - } - } - - /// Obtains the value of `__GPIO_MOCK_LAST` if present. - fn get_last(symbols: &Symbols) -> Option { - match symbols.get(&VarRef::new("__GPIO_MOCK_LAST", Some(ExprType::Integer))) { - Ok(Some(Symbol::Variable(Value::Integer(i)))) => Some(*i), - _ => None, - } - } +/// Self-contained mock GPIO implementation that records operations and supplies pre-seeded reads. +/// +/// Call `inject_read` to pre-seed reads before running code that calls `GPIO_READ`, and call +/// `trace` afterwards to inspect the ordered record of all GPIO operations. +#[derive(Default)] +pub struct MockPins { + /// FIFO queue of pre-seeded reads: the next `read` call pops from the front. + reads: VecDeque<(Pin, bool)>, - /// Obtains a mutable reference to `__GPIO_MOCK_DATA` if present. - fn get_mut_data(symbols: &mut Symbols) -> Option<&mut Array> { - match symbols.get_mut(&VarRef::new("__GPIO_MOCK_DATA", Some(ExprType::Integer))) { - Ok(Some(Symbol::Array(data))) if data.dimensions().len() == 1 => Some(data), - _ => None, - } - } + /// Ordered record of all GPIO operations performed. + /// + /// Operations are encoded as `pin * 100 + MockOp` with `ClearAll` being encoded as `-1`. + trace: Vec, +} - /// Reads the current value at `data[last]` with proper validation. - fn raw_get(last: i32, data: &Array) -> io::Result { - match data.index(&[last]) { - Ok(Value::Integer(v)) => Ok(*v), - Ok(_) => panic!("We know it's an integer"), - Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string())), - } +impl MockPins { + /// Pre-seeds a future `read(pin)` call to return `high`. + pub fn inject_read(&mut self, pin: Pin, high: bool) { + self.reads.push_back((pin, high)); } - /// Increments `__GPIO_MOCK_LAST`. - fn increment_last(&mut self) -> io::Result<()> { - let last = MockPins::get_last(self.symbols).expect("Validated at construction time"); - let new_last = Value::Integer(last + 1); - match self - .symbols - .set_var(&VarRef::new("__GPIO_MOCK_LAST", Some(ExprType::Integer)), new_last) - { - Ok(()) => Ok(()), - Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string())), - } + /// Returns the ordered trace of all GPIO operations performed so far. + pub fn trace(&self) -> &[i32] { + &self.trace } +} - /// Reads `__GPIO_MOCK_DATA[__GPIO_MOCK_LAST]` and advances `__GPIO_MOCK_LAST`. Returns the - /// position of the array that was read and the value at that position. - fn read_and_advance(&mut self) -> io::Result<(i32, i32)> { - let last = MockPins::get_last(self.symbols).expect("Validated at construction time"); - let data = MockPins::get_mut_data(self.symbols).expect("Validated at construction time"); - - let v = MockPins::raw_get(last, data)?; - self.increment_last()?; - Ok((last, v)) +impl Pins for MockPins { + fn as_any(&self) -> &dyn Any { + self } - /// Writes `datum` to `__GPIO_MOCK_DATA[__GPIO_MOCK_LAST]`, which must be zero upfront. - fn append(&mut self, datum: i32) -> io::Result<()> { - let last = MockPins::get_last(self.symbols).expect("Validated at construction time"); - let data = MockPins::get_mut_data(self.symbols).expect("Validated at construction time"); - - let old_datum = MockPins::raw_get(last, data)?; - if old_datum != 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Position already occupied at __GPIO_MOCK_READ({})", last), - )); - } - - let data = MockPins::get_mut_data(self.symbols).expect("Validated at construction time"); - match data.assign(&[last], Value::Integer(datum)) { - Ok(()) => (), - Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string())), - }; - self.increment_last() + fn as_any_mut(&mut self) -> &mut dyn Any { + self } -} -impl Pins for MockPins<'_> { fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()> { let datum = match mode { PinMode::In => MockOp::encode(pin, MockOp::SetupIn), @@ -222,39 +123,41 @@ impl Pins for MockPins<'_> { PinMode::InPullUp => MockOp::encode(pin, MockOp::SetupInPullUp), PinMode::Out => MockOp::encode(pin, MockOp::SetupOut), }; - self.append(datum) + self.trace.push(datum); + Ok(()) } fn clear(&mut self, pin: Pin) -> io::Result<()> { - let datum = MockOp::encode(pin, MockOp::Clear); - self.append(datum) + self.trace.push(MockOp::encode(pin, MockOp::Clear)); + Ok(()) } fn clear_all(&mut self) -> io::Result<()> { - let datum = MockOp::ClearAll as i32; - self.append(datum) + self.trace.push(MockOp::ClearAll as i32); + Ok(()) } fn read(&mut self, pin: Pin) -> io::Result { - let (pos, datum) = self.read_and_advance()?; - let (datum_pin, value) = MockOp::decode_read(pos, datum)?; - if datum_pin != pin { - return Err(io::Error::new( + match self.reads.pop_front() { + Some((read_pin, high)) if read_pin == pin => { + let op = if high { MockOp::ReadHigh } else { MockOp::ReadLow }; + self.trace.push(MockOp::encode(pin, op)); + Ok(high) + } + Some((read_pin, _)) => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Want to read pin {} but next mock read is for pin {}", pin.0, read_pin.0), + )), + None => Err(io::Error::new( io::ErrorKind::InvalidData, - format!( - "Want to read pin {} but __GPIO_MOCK_DATA({}) is for pin {}", - pin.0, pos, datum_pin.0 - ), - )); + format!("No mock read available for pin {}", pin.0), + )), } - Ok(value) } fn write(&mut self, pin: Pin, v: bool) -> io::Result<()> { - if v { - self.append(MockOp::encode(pin, MockOp::WriteHigh)) - } else { - self.append(MockOp::encode(pin, MockOp::WriteLow)) - } + let op = if v { MockOp::WriteHigh } else { MockOp::WriteLow }; + self.trace.push(MockOp::encode(pin, op)); + Ok(()) } } diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index e8fd5f96..ec6c1e1f 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -21,13 +21,14 @@ 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 std::any::Any; use std::borrow::Cow; use std::cell::RefCell; use std::io; use std::rc::Rc; mod fakes; -pub(crate) use fakes::{MockPins, NoopPins}; +pub use fakes::{MockPins, NoopPins}; /// Category description for all symbols provided by this module. const CATEGORY: &str = "Hardware interface @@ -83,6 +84,12 @@ impl PinMode { /// Generic abstraction over a GPIO chip to back all EndBASIC commands. pub trait Pins { + /// Returns `self` as `&dyn Any` to allow downcasting to a concrete type. + fn as_any(&self) -> &dyn Any; + + /// Returns `self` as `&mut dyn Any` to allow downcasting to a concrete type. + fn as_any_mut(&mut self) -> &mut dyn Any; + /// Configures the `pin` as either input or output (per `mode`). /// /// This lazily initialies the GPIO chip as well on the first pin setup. @@ -116,11 +123,8 @@ impl PinsClearable { } impl Clearable for PinsClearable { - fn reset_state(&self, syms: &mut Symbols) { - let _ = match MockPins::try_new(syms) { - Some(mut pins) => pins.clear_all(), - None => self.pins.borrow_mut().clear_all(), - }; + fn reset_state(&self, _syms: &mut Symbols) { + let _ = self.pins.borrow_mut().clear_all(); } } @@ -177,7 +181,7 @@ impl Callable for GpioSetupCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let pin = { let (i, pos) = scope.pop_integer_with_pos(); @@ -188,10 +192,7 @@ impl Callable for GpioSetupCommand { PinMode::parse(&t, pos)? }; - match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.setup(pin, mode).map_err(|e| scope.io_error(e))?, - None => self.pins.borrow_mut().setup(pin, mode).map_err(|e| scope.io_error(e))?, - }; + self.pins.borrow_mut().setup(pin, mode).map_err(|e| scope.io_error(e))?; Ok(()) } } @@ -239,12 +240,9 @@ impl Callable for GpioClearCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { if scope.nargs() == 0 { - match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.clear_all().map_err(|e| scope.io_error(e))?, - None => self.pins.borrow_mut().clear_all().map_err(|e| scope.io_error(e))?, - }; + self.pins.borrow_mut().clear_all().map_err(|e| scope.io_error(e))?; } else { debug_assert_eq!(1, scope.nargs()); let pin = { @@ -252,10 +250,7 @@ impl Callable for GpioClearCommand { Pin::from_i32(i, pos)? }; - match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.clear(pin).map_err(|e| scope.io_error(e))?, - None => self.pins.borrow_mut().clear(pin).map_err(|e| scope.io_error(e))?, - }; + self.pins.borrow_mut().clear(pin).map_err(|e| scope.io_error(e))?; } Ok(()) @@ -301,17 +296,14 @@ impl Callable for GpioReadFunction { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(1, scope.nargs()); let pin = { let (i, pos) = scope.pop_integer_with_pos(); Pin::from_i32(i, pos)? }; - let value = match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.read(pin).map_err(|e| scope.io_error(e))?, - None => self.pins.borrow_mut().read(pin).map_err(|e| scope.io_error(e))?, - }; + let value = self.pins.borrow_mut().read(pin).map_err(|e| scope.io_error(e))?; scope.return_boolean(value) } } @@ -363,7 +355,7 @@ impl Callable for GpioWriteCommand { &self.metadata } - async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> Result<()> { + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { debug_assert_eq!(2, scope.nargs()); let pin = { let (i, pos) = scope.pop_integer_with_pos(); @@ -371,16 +363,126 @@ impl Callable for GpioWriteCommand { }; let value = scope.pop_boolean(); - match MockPins::try_new(machine.get_mut_symbols()) { - Some(mut pins) => pins.write(pin, value).map_err(|e| scope.io_error(e))?, - None => self.pins.borrow_mut().write(pin, value).map_err(|e| scope.io_error(e))?, + self.pins.borrow_mut().write(pin, value).map_err(|e| scope.io_error(e))?; + Ok(()) + } +} + +/// The `GPIO_MOCK_INJECT` command. +pub struct GpioMockInjectCommand { + metadata: CallableMetadata, + pins: Rc>, +} + +impl GpioMockInjectCommand { + /// Creates a new instance of the command. + pub fn new(pins: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("GPIO_MOCK_INJECT") + .with_syntax(&[( + &[ + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("pin"), + vtype: ExprType::Integer, + }, + ArgSepSyntax::Exactly(ArgSep::Long), + ), + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("high"), + vtype: ExprType::Boolean, + }, + ArgSepSyntax::End, + ), + ], + None, + )]) + .with_category(CATEGORY) + .with_description( + "Pre-seeds a GPIO_READ result for testing. +This command is only available when EndBASIC is started with --gpio-pins=mock. It pre-seeds \ +the next GPIO_READ call for the given pin% to return the given high? value.", + ) + .build(), + pins, + }) + } +} + +#[async_trait(?Send)] +impl Callable for GpioMockInjectCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + debug_assert_eq!(2, scope.nargs()); + let pin = { + let (i, pos) = scope.pop_integer_with_pos(); + Pin::from_i32(i, pos)? }; + let high = scope.pop_boolean(); + + self.pins + .borrow_mut() + .as_any_mut() + .downcast_mut::() + .expect("Only registered for mock backend") + .inject_read(pin, high); Ok(()) } } +/// The `GPIO_MOCK_TRACE` function. +pub struct GpioMockTraceFunction { + metadata: CallableMetadata, + pins: Rc>, +} + +impl GpioMockTraceFunction { + /// Creates a new instance of the function. + pub fn new(pins: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("GPIO_MOCK_TRACE") + .with_return_type(ExprType::Text) + .with_syntax(&[(&[], None)]) + .with_category(CATEGORY) + .with_description( + "Returns the GPIO operation trace for testing. +This function is only available when EndBASIC is started with --gpio-pins=mock. It returns a \ +space-separated list of integers representing the ordered record of all GPIO operations \ +performed since the last reset.", + ) + .build(), + pins, + }) + } +} + +#[async_trait(?Send)] +impl Callable for GpioMockTraceFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> { + debug_assert_eq!(0, scope.nargs()); + let pins = self.pins.borrow(); + let mock = + pins.as_any().downcast_ref::().expect("Only registered for mock backend"); + let result = mock.trace().iter().map(|v| v.to_string()).collect::>().join(" "); + scope.return_string(result) + } +} + /// Adds all symbols provided by this module to the given `machine`. pub fn add_all(machine: &mut Machine, pins: Rc>) { + if pins.borrow().as_any().downcast_ref::().is_some() { + machine.add_callable(GpioMockInjectCommand::new(pins.clone())); + machine.add_callable(GpioMockTraceFunction::new(pins.clone())); + } + machine.add_clearable(PinsClearable::new(pins.clone())); machine.add_callable(GpioClearCommand::new(pins.clone())); machine.add_callable(GpioReadFunction::new(pins.clone())); @@ -392,7 +494,7 @@ pub fn add_all(machine: &mut Machine, pins: Rc>) { mod tests { use super::*; use crate::testutils::*; - use endbasic_core::ast::Value; + use futures_lite::future::block_on; /// Common checks for pin number validation. /// @@ -414,48 +516,29 @@ mod tests { ); } - /// Does a GPIO test using the mocking feature, running the commands in `code` and expecting - /// that the `__GPIO_MOCK_DATA` array contains `trace` after completion. - /// - /// Sets all `vars` before evaluating the expression so that the expression can contain variable - /// references. - fn do_mock_test_with_vars, VS: Into>>( - code: S, - trace: &[i32], - vars: VS, - ) { - let code = code.into(); - let vars = vars.into(); - - let mut exp_data = vec![Value::Integer(0); 50]; - for (i, d) in trace.iter().enumerate() { - exp_data[i] = Value::Integer(*d); - } - - let mut t = Tester::default(); - for var in vars.as_slice() { - t = t.set_var(var.0, var.1.clone()); - } - - let mut c = t - .run(format!(r#"DIM __GPIO_MOCK_DATA(50) AS INTEGER: __GPIO_MOCK_LAST = 0: {}"#, code)); - for var in vars.into_iter() { - c = c.expect_var(var.0, var.1.clone()); + /// Creates a machine backed by `MockPins` pre-seeded with `reads` and returns both the machine + /// and a handle to inspect the trace afterwards. + fn make_mock_machine(reads: &[(u8, bool)]) -> (Machine, Rc>) { + let mock_pins = Rc::new(RefCell::new(MockPins::default())); + for &(pin, high) in reads { + mock_pins.borrow_mut().inject_read(Pin(pin), high); } - c.expect_var("__GPIO_MOCK_LAST", Value::Integer(trace.len() as i32)) - .expect_array_simple("__GPIO_MOCK_DATA", ExprType::Integer, exp_data) - .check(); + let pins: Rc> = mock_pins.clone(); + let machine = crate::MachineBuilder::default().with_gpio_pins(pins).build().unwrap(); + (machine, mock_pins) } - /// Does a GPIO test using the mocking feature, running the commands in `code` and expecting - /// that the `__GPIO_MOCK_DATA` array contains `trace` after completion. - fn do_mock_test>(code: S, trace: &[i32]) { - do_mock_test_with_vars(code, trace, []) + /// Runs `code` in a machine backed by MockPins pre-seeded with `reads` and asserts that the + /// resulting trace equals `expected_trace`. + fn do_mock_test(code: &str, reads: &[(u8, bool)], expected_trace: &[i32]) { + let (mut machine, mock_pins) = make_mock_machine(reads); + let _ = block_on(machine.exec(&mut code.as_bytes())).unwrap(); + assert_eq!(expected_trace, mock_pins.borrow().trace()); } /// Tests that all GPIO operations delegate to the real pins implementation, which defaults to - /// the no-op backend when using the tester. All other tests in this file use the mocking - /// features to validate operation. + /// the no-op backend when using the tester. All other tests in this file use MockPins via + /// `make_mock_machine` to validate operation. #[test] fn test_real_backend() { check_stmt_err("1:1: GPIO backend not compiled in", "GPIO_SETUP 0, \"IN\""); @@ -468,26 +551,26 @@ mod tests { #[test] fn test_gpio_setup_ok() { for mode in &["in", "IN"] { - do_mock_test(format!(r#"GPIO_SETUP 5, "{}""#, mode), &[501]); - do_mock_test(format!(r#"GPIO_SETUP 5.2, "{}""#, mode), &[501]); + do_mock_test(&format!(r#"GPIO_SETUP 5, "{}""#, mode), &[], &[501]); + do_mock_test(&format!(r#"GPIO_SETUP 5.2, "{}""#, mode), &[], &[501]); } for mode in &["in-pull-down", "IN-PULL-DOWN"] { - do_mock_test(format!(r#"GPIO_SETUP 6, "{}""#, mode), &[602]); - do_mock_test(format!(r#"GPIO_SETUP 6.2, "{}""#, mode), &[602]); + do_mock_test(&format!(r#"GPIO_SETUP 6, "{}""#, mode), &[], &[602]); + do_mock_test(&format!(r#"GPIO_SETUP 6.2, "{}""#, mode), &[], &[602]); } for mode in &["in-pull-up", "IN-PULL-UP"] { - do_mock_test(format!(r#"GPIO_SETUP 7, "{}""#, mode), &[703]); - do_mock_test(format!(r#"GPIO_SETUP 7.2, "{}""#, mode), &[703]); + do_mock_test(&format!(r#"GPIO_SETUP 7, "{}""#, mode), &[], &[703]); + do_mock_test(&format!(r#"GPIO_SETUP 7.2, "{}""#, mode), &[], &[703]); } for mode in &["out", "OUT"] { - do_mock_test(format!(r#"GPIO_SETUP 8, "{}""#, mode), &[804]); - do_mock_test(format!(r#"GPIO_SETUP 8.2, "{}""#, mode), &[804]); + do_mock_test(&format!(r#"GPIO_SETUP 8, "{}""#, mode), &[], &[804]); + do_mock_test(&format!(r#"GPIO_SETUP 8.2, "{}""#, mode), &[], &[804]); } } #[test] fn test_gpio_setup_multiple() { - do_mock_test(r#"GPIO_SETUP 18, "IN-PULL-UP": GPIO_SETUP 10, "OUT""#, &[1803, 1004]); + do_mock_test(r#"GPIO_SETUP 18, "IN-PULL-UP": GPIO_SETUP 10, "OUT""#, &[], &[1803, 1004]); } #[test] @@ -504,13 +587,13 @@ mod tests { #[test] fn test_gpio_clear_all() { - do_mock_test("GPIO_CLEAR", &[-1]); + do_mock_test("GPIO_CLEAR", &[], &[-1]); } #[test] fn test_gpio_clear_one() { - do_mock_test("GPIO_CLEAR 4", &[405]); - do_mock_test("GPIO_CLEAR 4.1", &[405]); + do_mock_test("GPIO_CLEAR 4", &[], &[405]); + do_mock_test("GPIO_CLEAR 4.1", &[], &[405]); } #[test] @@ -523,13 +606,12 @@ mod tests { #[test] fn test_gpio_read_ok() { - do_mock_test_with_vars( - "__GPIO_MOCK_DATA(0) = 310 - __GPIO_MOCK_DATA(2) = 311 - GPIO_WRITE 5, GPIO_READ(3.1) - GPIO_WRITE 7, GPIO_READ(pin)", + // Read pin 3 (low → GPIO_WRITE 5 low), then read pin 3 (high → GPIO_WRITE 7 high). + // GPIO_READ evaluates before GPIO_WRITE, so trace is: read3low, write5low, read3high, write7high. + do_mock_test( + "GPIO_WRITE 5, GPIO_READ(3.1): GPIO_WRITE 7, GPIO_READ(3)", + &[(3, false), (3, true)], &[310, 520, 311, 721], - [("pin", 3.into())], ); } @@ -543,7 +625,7 @@ mod tests { #[test] fn test_gpio_write_ok() { - do_mock_test("GPIO_WRITE 3, TRUE: GPIO_WRITE 3.1, FALSE", &[321, 320]); + do_mock_test("GPIO_WRITE 3, TRUE: GPIO_WRITE 3.1, FALSE", &[], &[321, 320]); } #[test] diff --git a/std/src/testutils.rs b/std/src/testutils.rs index 4856081d..c3a1230f 100644 --- a/std/src/testutils.rs +++ b/std/src/testutils.rs @@ -396,11 +396,9 @@ impl Default for Tester { let console = Rc::from(RefCell::from(MockConsole::default())); let program = Rc::from(RefCell::from(RecordedProgram::default())); - // Default to the pins set that always returns errors. We could have implemented a set of - // fake pins here to track GPIO state changes in a nicer way, similar to how we track all - // other machine state... but the GPIO module already implements its own mocking feature. - // The mocking feature is necessary for integration testing in any case, so we just use that - // everywhere instead of having yet another implementation in this module. + // Default to the no-op pins that always return errors. GPIO unit tests use MockPins + // directly via `make_mock_machine` to validate operation; this Tester wiring is only used + // for the error-path tests that go through the real (NoopPins) backend. let gpio_pins = Rc::from(RefCell::from(gpio::NoopPins::default())); let mut builder = crate::MachineBuilder::default() From dd332f1fd57426b33d007fe0d58031333d6dbfcf Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 18 Apr 2026 08:41:12 -0700 Subject: [PATCH 085/110] Appease collapsible_match warning in Rust 1.95 --- core/src/parser.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/core/src/parser.rs b/core/src/parser.rs index 76a64590..edfa9de6 100644 --- a/core/src/parser.rs +++ b/core/src/parser.rs @@ -794,15 +794,13 @@ impl<'a> Parser<'a> { | 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; - } + 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; } _ => (), }; From 72e23d10a8932e2b280a82f9ed9e91f6c0cf3f63 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 29 Mar 2026 09:16:03 -0700 Subject: [PATCH 086/110] Make error positions more precise for invalid separators --- client/src/cmds.rs | 4 ++-- core/src/compiler/args.rs | 11 ++++------- std/src/console/cmds.rs | 10 +++++----- std/src/data.rs | 2 +- std/src/gfx/mod.rs | 9 +++++++-- std/src/gpio/mod.rs | 2 +- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/client/src/cmds.rs b/client/src/cmds.rs index fe4b6426..1221bdfd 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -955,11 +955,11 @@ mod tests { r#"SHARE , "a""#, ); client_check_stmt_compilation_err( - "1:1: SHARE expected filename$[, acl1$, .., aclN$]", + "1:10: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE "a"; "b""#, ); client_check_stmt_compilation_err( - "1:1: SHARE expected filename$[, acl1$, .., aclN$]", + "1:15: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE "a", "b"; "c""#, ); client_check_stmt_compilation_err( diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index 1c1a8be0..f475c728 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -442,7 +442,6 @@ fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&Cal fn compile_syn_argsep( instrs: &mut Vec, md: &CallableMetadata, - pos: LineCol, syn: &ArgSepSyntax, is_last: bool, sep: ArgSep, @@ -458,7 +457,7 @@ fn compile_syn_argsep( ArgSepSyntax::Exactly(exp_sep) => { debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); if sep != ArgSep::End && sep != *exp_sep { - return Err(Error::CallableSyntaxError(pos, md.clone())); + return Err(Error::CallableSyntaxError(sep_pos, md.clone())); } Ok(0) } @@ -470,7 +469,7 @@ fn compile_syn_argsep( Ok(0) } else { if sep != *exp_sep1 && sep != *exp_sep2 { - return Err(Error::CallableSyntaxError(pos, md.clone())); + return Err(Error::CallableSyntaxError(sep_pos, md.clone())); } instrs.insert(sep_tag_pc, Instruction::PushInteger(sep as i32, sep_pos)); Ok(1) @@ -576,7 +575,6 @@ fn compile_args( nargs += compile_syn_argsep( instrs, md, - pos, &syn.sep, input_nargs == remaining, span.sep, @@ -663,7 +661,6 @@ fn compile_args( nargs += compile_syn_argsep( instrs, md, - pos, exp_sep, input_nargs == remaining, span.sep, @@ -1777,7 +1774,7 @@ mod compile_tests { ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 4) }, ]) .exp_error(Error::CallableSyntaxError( - lc(1000, 2000), + lc(1, 1), CallableMetadataBuilder::new("TEST") .with_syntax(&[( &[ @@ -1818,7 +1815,7 @@ mod compile_tests { ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 4) }, ]) .exp_error(Error::CallableSyntaxError( - lc(1000, 2000), + lc(1, 1), CallableMetadataBuilder::new("TEST") .with_syntax(&[( &[ diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index eb729a33..eee30cc3 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -719,7 +719,7 @@ mod tests { "1:1: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1, 2, 3", ); - check_stmt_compilation_err("1:1: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1; 2"); + check_stmt_compilation_err("1:8: COLOR expected <> | | <[fg%], [bg%]>", "COLOR 1; 2"); check_stmt_err("1:7: Color out of range", "COLOR 1000, 0"); check_stmt_err("1:10: Color out of range", "COLOR 0, 1000"); @@ -855,7 +855,7 @@ mod tests { ); check_stmt_compilation_err("1:7: expected STRING but found INTEGER", "INPUT 3 ; a"); check_stmt_compilation_err( - "1:1: INPUT expected | <[prompt$] <,|;> vref>", + "1:13: INPUT expected | <[prompt$] <,|;> vref>", "INPUT \"foo\" AS bar", ); check_stmt_err("1:7: Undefined symbol A", "INPUT a + 1 ; b"); @@ -884,7 +884,7 @@ mod tests { check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE"); check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1"); check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1, 2, 3"); - check_stmt_compilation_err("1:1: LOCATE expected column%, row%", "LOCATE 1; 2"); + check_stmt_compilation_err("1:9: LOCATE expected column%, row%", "LOCATE 1; 2"); check_stmt_err("1:8: Column out of range", "LOCATE -1, 2"); check_stmt_err("1:8: Column out of range", "LOCATE 70000, 2"); @@ -994,11 +994,11 @@ mod tests { #[test] fn test_print_errors() { check_stmt_compilation_err( - "1:1: PRINT expected [expr1 <,|;> .. <,|;> exprN]", + "1:9: PRINT expected [expr1 <,|;> .. <,|;> exprN]", "PRINT 3 AS 4", ); check_stmt_compilation_err( - "1:1: PRINT expected [expr1 <,|;> .. <,|;> exprN]", + "1:12: PRINT expected [expr1 <,|;> .. <,|;> exprN]", "PRINT 3, 4 AS 5", ); // Ensure type errors from `Expr` and `Value` bubble up. diff --git a/std/src/data.rs b/std/src/data.rs index 556fc0aa..6369da2b 100644 --- a/std/src/data.rs +++ b/std/src/data.rs @@ -304,7 +304,7 @@ mod tests { fn test_read_errors() { check_stmt_compilation_err("1:1: READ expected vref1[, .., vrefN]", "READ"); check_stmt_compilation_err("1:6: Requires a reference, not a value", "READ 3"); - check_stmt_compilation_err("1:1: READ expected vref1[, .., vrefN]", "READ i; j"); + check_stmt_compilation_err("1:7: READ expected vref1[, .., vrefN]", "READ i; j"); check_stmt_err( "1:16: Cannot assign value of type STRING to variable of type INTEGER", diff --git a/std/src/gfx/mod.rs b/std/src/gfx/mod.rs index 6687e2c1..a5761a9b 100644 --- a/std/src/gfx/mod.rs +++ b/std/src/gfx/mod.rs @@ -683,12 +683,16 @@ mod tests { /// Verifies error conditions for a command named `name` that takes an X/Y pair and a radius. fn check_errors_xy_radius(name: &'static str) { - for args in &["1, , 3", "1, 2", "1, 2, 3, 4", "2; 3, 4"] { + for args in &["1, , 3", "1, 2", "1, 2, 3, 4"] { check_stmt_compilation_err( format!("1:1: {} expected x%, y%, r%", name), &format!("{} {}", name, args), ); } + check_stmt_compilation_err( + format!("1:{}: {} expected x%, y%, r%", name.len() + 3, name), + &format!("{} 2; 3, 4", name), + ); for args in &["-40000, 1, 1", "1, -40000, 1"] { let pos = name.len() + 1 + args.find('-').unwrap() + 1; @@ -831,9 +835,10 @@ mod tests { #[test] fn test_gfx_pixel_errors() { - for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1", "GFX_PIXEL 1; 2"] { + for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1"] { check_stmt_compilation_err("1:1: GFX_PIXEL expected x%, y%", cmd); } + check_stmt_compilation_err("1:12: GFX_PIXEL expected x%, y%", "GFX_PIXEL 1; 2"); for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] { check_stmt_err( diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index ec6c1e1f..4a3f6ef8 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -637,7 +637,7 @@ mod tests { r#"GPIO_WRITE 1, TRUE, 2"#, ); check_stmt_compilation_err( - "1:1: GPIO_WRITE expected pin%, value?", + "1:13: GPIO_WRITE expected pin%, value?", r#"GPIO_WRITE 1; TRUE"#, ); From ef993bd4361237a16a329bd57693d37b44cbfe14 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 17 Apr 2026 20:07:45 +0200 Subject: [PATCH 087/110] Fix case of TypeMismatch error message All other compiler error messages start with a capital letter except this one. Fix it for consistency. --- cli/tests/lang/operators.out | 8 ++++---- client/src/cmds.rs | 10 +++++----- core/src/compiler/mod.rs | 2 +- std/src/console/cmds.rs | 2 +- std/src/gfx/mod.rs | 2 +- std/src/gpio/mod.rs | 4 ++-- std/src/help.rs | 2 +- std/src/program.rs | 2 +- std/src/storage/cmds.rs | 12 ++++++------ std/src/strings.rs | 12 ++++++------ 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/cli/tests/lang/operators.out b/cli/tests/lang/operators.out index c4674138..a52cb961 100644 --- a/cli/tests/lang/operators.out +++ b/cli/tests/lang/operators.out @@ -56,8 +56,8 @@ ERROR: 1:13: Cannot << BOOLEAN ERROR: 1:13: Cannot << BOOLEAN ERROR: 1:11: Cannot << DOUBLE ERROR: 1:13: Cannot << STRING -ERROR: 1:9: expected INTEGER but found BOOLEAN -ERROR: 1:9: expected INTEGER but found DOUBLE +ERROR: 1:9: Expected INTEGER but found BOOLEAN +ERROR: 1:9: Expected INTEGER but found DOUBLE ERROR: 1:9: Number of bits to << (-1) must be positive >>> Test bitwise shift right 3 @@ -76,8 +76,8 @@ ERROR: 1:13: Cannot >> BOOLEAN ERROR: 1:13: Cannot >> BOOLEAN ERROR: 1:11: Cannot >> DOUBLE ERROR: 1:13: Cannot >> STRING -ERROR: 1:9: expected INTEGER but found BOOLEAN -ERROR: 1:9: expected INTEGER but found DOUBLE +ERROR: 1:9: Expected INTEGER but found BOOLEAN +ERROR: 1:9: Expected INTEGER but found DOUBLE ERROR: 1:9: Number of bits to >> (-1) must be positive >>> Test = types TRUE diff --git a/client/src/cmds.rs b/client/src/cmds.rs index 1221bdfd..4b797260 100644 --- a/client/src/cmds.rs +++ b/client/src/cmds.rs @@ -766,13 +766,13 @@ mod tests { "1:1: LOGIN expected | ", r#"LOGIN ;"#, ); - client_check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"LOGIN 3"#); + client_check_stmt_compilation_err("1:7: Expected STRING but found INTEGER", r#"LOGIN 3"#); client_check_stmt_compilation_err( - "1:7: expected STRING but found INTEGER", + "1:7: Expected STRING but found INTEGER", r#"LOGIN 3, "a""#, ); client_check_stmt_compilation_err( - "1:12: expected STRING but found INTEGER", + "1:12: Expected STRING but found INTEGER", r#"LOGIN "a", 3"#, ); } @@ -949,7 +949,7 @@ mod tests { "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE"#, ); - client_check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"SHARE 1"#); + client_check_stmt_compilation_err("1:7: Expected STRING but found INTEGER", r#"SHARE 1"#); client_check_stmt_compilation_err( "1:1: SHARE expected filename$[, acl1$, .., aclN$]", r#"SHARE , "a""#, @@ -967,7 +967,7 @@ mod tests { r#"SHARE "a", , "b""#, ); client_check_stmt_compilation_err( - "1:12: expected STRING but found INTEGER", + "1:12: Expected STRING but found INTEGER", r#"SHARE "a", 3, "b""#, ); client_check_stmt_err( diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index f6512133..083a499c 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -86,7 +86,7 @@ pub enum Error { #[error("{0}: Cannot define already-defined symbol {1}")] RedefinitionError(LineCol, SymbolKey), - #[error("{0}: expected {2} but found {1}")] + #[error("{0}: Expected {2} but found {1}")] TypeMismatch(LineCol, ExprType, ExprType), #[error("{0}: Cannot {1} {2}")] diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index eee30cc3..9d75088c 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -853,7 +853,7 @@ mod tests { "1:1: INPUT expected | <[prompt$] <,|;> vref>", "INPUT ;", ); - check_stmt_compilation_err("1:7: expected STRING but found INTEGER", "INPUT 3 ; a"); + check_stmt_compilation_err("1:7: Expected STRING but found INTEGER", "INPUT 3 ; a"); check_stmt_compilation_err( "1:13: INPUT expected | <[prompt$] <,|;> vref>", "INPUT \"foo\" AS bar", diff --git a/std/src/gfx/mod.rs b/std/src/gfx/mod.rs index a5761a9b..ffbb353c 100644 --- a/std/src/gfx/mod.rs +++ b/std/src/gfx/mod.rs @@ -917,7 +917,7 @@ mod tests { #[test] fn test_gfx_sync_errors() { check_stmt_compilation_err("1:1: GFX_SYNC expected <> | ", "GFX_SYNC 2, 3"); - check_stmt_compilation_err("1:10: expected BOOLEAN but found INTEGER", "GFX_SYNC 2"); + check_stmt_compilation_err("1:10: Expected BOOLEAN but found INTEGER", "GFX_SYNC 2"); } #[test] diff --git a/std/src/gpio/mod.rs b/std/src/gpio/mod.rs index 4a3f6ef8..6e023d0d 100644 --- a/std/src/gpio/mod.rs +++ b/std/src/gpio/mod.rs @@ -577,7 +577,7 @@ mod tests { fn test_gpio_setup_errors() { check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP"#); check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1"#); - check_stmt_compilation_err("1:15: expected STRING but found INTEGER", r#"GPIO_SETUP 1; 2"#); + check_stmt_compilation_err("1:15: Expected STRING but found INTEGER", r#"GPIO_SETUP 1; 2"#); check_stmt_compilation_err("1:1: GPIO_SETUP expected pin%, mode$", r#"GPIO_SETUP 1, 2, 3"#); check_pin_validation("1:12: ", "1:12: ", r#"GPIO_SETUP _PIN_, "IN""#); @@ -644,7 +644,7 @@ mod tests { check_pin_validation("1:12: ", "1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#); check_stmt_compilation_err( - "1:15: expected BOOLEAN but found INTEGER", + "1:15: Expected BOOLEAN but found INTEGER", r#"GPIO_WRITE 1, 5"#, ); } diff --git a/std/src/help.rs b/std/src/help.rs index d65d32f0..c3caceec 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -936,7 +936,7 @@ This is the first and only topic with just one line. t.run(r#"HELP "foo", 3"#) .expect_compilation_err("1:1: HELP expected <> | ") .check(); - t.run(r#"HELP 3"#).expect_compilation_err("1:6: expected STRING but found INTEGER").check(); + t.run(r#"HELP 3"#).expect_compilation_err("1:6: Expected STRING but found INTEGER").check(); t.run(r#"HELP "lang%""#).expect_err("1:6: Unknown help topic lang%").check(); diff --git a/std/src/program.rs b/std/src/program.rs index 2a249785..3569bfad 100644 --- a/std/src/program.rs +++ b/std/src/program.rs @@ -793,7 +793,7 @@ mod tests { Tester::default() .run(format!("{} 3", cmd)) .expect_compilation_err(format!( - "1:{}: expected STRING but found INTEGER", + "1:{}: Expected STRING but found INTEGER", cmd.len() + 2, )) .check(); diff --git a/std/src/storage/cmds.rs b/std/src/storage/cmds.rs index 153de247..1ce1d286 100644 --- a/std/src/storage/cmds.rs +++ b/std/src/storage/cmds.rs @@ -544,7 +544,7 @@ mod tests { check_stmt_err("1:1: Drive 'A' is not mounted", "CD \"A:\""); check_stmt_compilation_err("1:1: CD expected path$", "CD"); check_stmt_compilation_err("1:1: CD expected path$", "CD 2, 3"); - check_stmt_compilation_err("1:4: expected STRING but found INTEGER", "CD 2"); + check_stmt_compilation_err("1:4: Expected STRING but found INTEGER", "CD 2"); } #[test] @@ -831,7 +831,7 @@ mod tests { #[test] fn test_dir_errors() { check_stmt_compilation_err("1:1: DIR expected <> | ", "DIR 2, 3"); - check_stmt_compilation_err("1:5: expected STRING but found INTEGER", "DIR 2"); + check_stmt_compilation_err("1:5: Expected STRING but found INTEGER", "DIR 2"); } #[test] @@ -852,7 +852,7 @@ mod tests { fn test_kill_errors() { Tester::default() .run("KILL 3") - .expect_compilation_err("1:6: expected STRING but found INTEGER") + .expect_compilation_err("1:6: Expected STRING but found INTEGER") .check(); Tester::default() @@ -929,8 +929,8 @@ mod tests { "MOUNT 1, 2, 3", ); - check_stmt_compilation_err("1:14: expected STRING but found INTEGER", r#"MOUNT "a" AS 1"#); - check_stmt_compilation_err("1:7: expected STRING but found INTEGER", r#"MOUNT 1 AS "a""#); + check_stmt_compilation_err("1:14: Expected STRING but found INTEGER", r#"MOUNT "a" AS 1"#); + check_stmt_compilation_err("1:7: Expected STRING but found INTEGER", r#"MOUNT 1 AS "a""#); check_stmt_err("1:1: Invalid drive name 'a:'", r#"MOUNT "memory://" AS "a:""#); check_stmt_err( @@ -995,7 +995,7 @@ mod tests { check_stmt_compilation_err("1:1: UNMOUNT expected drive_name$", "UNMOUNT"); check_stmt_compilation_err("1:1: UNMOUNT expected drive_name$", "UNMOUNT 2, 3"); - check_stmt_compilation_err("1:9: expected STRING but found INTEGER", "UNMOUNT 1"); + check_stmt_compilation_err("1:9: Expected STRING but found INTEGER", "UNMOUNT 1"); check_stmt_err("1:1: Invalid drive name 'a:'", "UNMOUNT \"a:\""); check_stmt_err("1:1: Drive 'a' is not mounted", "UNMOUNT \"a\""); diff --git a/std/src/strings.rs b/std/src/strings.rs index b2d91343..36366d51 100644 --- a/std/src/strings.rs +++ b/std/src/strings.rs @@ -691,7 +691,7 @@ mod tests { check_expr_ok_with_vars('a' as i32, r#"ASC(s)"#, [("s", "a".into())]); check_expr_compilation_error("1:10: ASC expected char$", r#"ASC()"#); - check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"ASC(3)"#); + check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"ASC(3)"#); check_expr_compilation_error("1:10: ASC expected char$", r#"ASC("a", 1)"#); check_expr_error("1:14: Input string \"\" must be 1-character long", r#"ASC("")"#); check_expr_error("1:14: Input string \"ab\" must be 1-character long", r#"ASC("ab")"#); @@ -731,7 +731,7 @@ mod tests { check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT()"#); check_expr_compilation_error("1:10: LEFT expected expr$, n%", r#"LEFT("", 1, 2)"#); - check_expr_compilation_error("1:15: expected STRING but found INTEGER", r#"LEFT(1, 2)"#); + check_expr_compilation_error("1:15: Expected STRING but found INTEGER", r#"LEFT(1, 2)"#); check_expr_compilation_error("1:19: STRING is not a number", r#"LEFT("", "")"#); check_expr_error("1:25: n% cannot be negative", r#"LEFT("abcdef", -5)"#); } @@ -745,7 +745,7 @@ mod tests { check_expr_ok_with_vars(4, r#"LEN(s)"#, [("s", "1234".into())]); check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN()"#); - check_expr_compilation_error("1:14: expected STRING but found INTEGER", r#"LEN(3)"#); + check_expr_compilation_error("1:14: Expected STRING but found INTEGER", r#"LEN(3)"#); check_expr_compilation_error("1:10: LEN expected expr$", r#"LEN(" ", 1)"#); } @@ -759,7 +759,7 @@ mod tests { check_expr_ok_with_vars("foo ", r#"LTRIM(s)"#, [("s", " foo ".into())]); check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM()"#); - check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"LTRIM(3)"#); + check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"LTRIM(3)"#); check_expr_compilation_error("1:10: LTRIM expected expr$", r#"LTRIM(" ", 1)"#); } @@ -812,7 +812,7 @@ mod tests { check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT()"#); check_expr_compilation_error("1:10: RIGHT expected expr$, n%", r#"RIGHT("", 1, 2)"#); - check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RIGHT(1, 2)"#); + check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"RIGHT(1, 2)"#); check_expr_compilation_error("1:20: STRING is not a number", r#"RIGHT("", "")"#); check_expr_error("1:26: n% cannot be negative", r#"RIGHT("abcdef", -5)"#); } @@ -827,7 +827,7 @@ mod tests { check_expr_ok_with_vars(" foo", r#"RTRIM(s)"#, [("s", " foo ".into())]); check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM()"#); - check_expr_compilation_error("1:16: expected STRING but found INTEGER", r#"RTRIM(3)"#); + check_expr_compilation_error("1:16: Expected STRING but found INTEGER", r#"RTRIM(3)"#); check_expr_compilation_error("1:10: RTRIM expected expr$", r#"RTRIM(" ", 1)"#); } From bf6a3c3babe381c7fabd9bfbd34cb05975e2f6b1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 17:43:46 -0700 Subject: [PATCH 088/110] Modernize GitHub actions versions --- .github/workflows/build.yml | 20 ++++++++++---------- .github/workflows/deploy-release.yml | 2 +- .github/workflows/deploy-staging.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc144b92..bd0df311 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,11 +25,11 @@ jobs: with: toolchain: stable target: armv7-unknown-linux-gnueabihf - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install gcc-arm-linux-gnueabihf - run: ./.github/workflows/release.sh linux-armv7-rpi - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: endbasic-linux-armv7-rpi path: endbasic-linux-armv7-rpi/* @@ -38,11 +38,11 @@ jobs: linux-x86_64-sdl: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/release.sh linux-x86_64-sdl - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: endbasic-linux-x86_64-sdl path: endbasic-linux-x86_64-sdl/* @@ -51,9 +51,9 @@ jobs: macos-arm64-sdl: runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: ./.github/workflows/release.sh macos-arm64-sdl - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: endbasic-macos-arm64-sdl path: endbasic-macos-arm64-sdl/* @@ -62,12 +62,12 @@ jobs: windows-x86_64-sdl: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: choco install --allow-empty-checksums unzip zip - run: ./.github/workflows/setup-sdl.ps1 - run: ./.github/workflows/release.sh windows-x86_64-sdl shell: bash - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: endbasic-windows-x86_64-sdl path: endbasic-windows-x86_64-sdl/* @@ -78,10 +78,10 @@ jobs: needs: [linux-armv7-rpi, linux-x86_64-sdl, macos-arm64-sdl, windows-x86_64-sdl] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: ./.github/workflows/notes.sh >notes.md || touch notes.md - name: Fetch binary artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts - name: Create release draft diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 9136c9e5..6e9aca2b 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -24,7 +24,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: ./.github/workflows/publish.sh prod - uses: JamesIves/github-pages-deploy-action@4.1.4 with: diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 0bab942f..fb0335aa 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -27,7 +27,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: ./.github/workflows/publish.sh staging - uses: JamesIves/github-pages-deploy-action@4.1.4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 105688dd..7c1acaff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: cargo-package: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/package.sh @@ -33,7 +33,7 @@ jobs: cargo-install: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/install.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 917ed9cf..7e3726e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: # verify any new Clippy warnings that may appear. toolchain: stable components: clippy, rustfmt - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/lint.sh @@ -41,7 +41,7 @@ jobs: TEST_ACCOUNT_2_USERNAME: ${{ secrets.TEST_ACCOUNT_2_USERNAME }} TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: cargo test --package='*' --features=sdl @@ -56,7 +56,7 @@ jobs: TEST_ACCOUNT_2_USERNAME: ${{ secrets.TEST_ACCOUNT_2_USERNAME }} TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: brew install sdl2 sdl2_ttf - run: cargo test --package=endbasic-client -- --include-ignored - run: cargo test --package=endbasic-core -- --include-ignored @@ -80,7 +80,7 @@ jobs: TEST_ACCOUNT_2_USERNAME: ${{ secrets.TEST_ACCOUNT_2_USERNAME }} TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: choco install --allow-empty-checksums unzip - run: ./.github/workflows/setup-sdl.ps1 - run: cargo test --package=endbasic-client -- --include-ignored @@ -97,5 +97,5 @@ jobs: linux-test-no-features: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: cd std && cargo build --no-default-features From 281874805f8f0f1bae8d07833ffc7c86a292e0ad Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 17:48:59 -0700 Subject: [PATCH 089/110] Update npm dependencies --- web/package-lock.json | 3084 ++++++++++++++++++++++++----------------- web/package.json | 6 +- 2 files changed, 1809 insertions(+), 1281 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index a842d60a..aef82d52 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^1.7.0", - "copy-webpack-plugin": "^12.0.2", + "copy-webpack-plugin": "^14.0.0", "html-webpack-plugin": "^5.6.3", "rimraf": "^6.0.1", "webpack": "^5.96.1", @@ -26,117 +26,275 @@ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", + "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "ansi-regex": "^6.0.1" + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" }, "engines": { - "node": ">=12" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", + "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "thingies": "^2.5.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", + "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", + "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=6.0.0" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", + "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", + "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.2" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", + "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jsonjoy.com/fs-node-utils": "4.57.2", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", + "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, "engines": { "node": ">=10.0" }, @@ -148,16 +306,55 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.0.tgz", - "integrity": "sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/base64": "^1.1.1", - "@jsonjoy.com/util": "^1.1.2", + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", "hyperdyperid": "^1.2.0", - "thingies": "^1.20.0" + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" }, "engines": { "node": ">=10.0" @@ -170,11 +367,15 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/util": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", - "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, "engines": { "node": ">=10.0" }, @@ -186,64 +387,315 @@ "tslib": "2" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" }, "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" }, "engines": { - "node": ">= 8" + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, + "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -254,6 +706,7 @@ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -263,6 +716,7 @@ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -272,6 +726,7 @@ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, + "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -282,6 +737,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -292,46 +748,38 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -343,19 +791,22 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -364,57 +815,54 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", "dev": true, - "dependencies": { - "undici-types": "~6.19.8" - } + "license": "MIT" }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*" + "undici-types": "~7.19.0" } }, "node_modules/@types/qs": { - "version": "6.9.17", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", - "dev": true + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -423,19 +871,32 @@ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/sockjs": { @@ -443,15 +904,17 @@ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -461,6 +924,7 @@ "resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.7.0.tgz", "integrity": "sha512-WikzYsw7nTd5CZxH75h7NxM/FLJAgqfWt+/gk3EL3wYKxiIlpMIYPja+sHQl3ARiicIYy4BDfxkbAVjRYlouTA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^2.4.1", "command-exists": "^1.2.7", @@ -473,6 +937,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -482,25 +947,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -511,13 +980,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -530,6 +1001,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -539,6 +1011,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } @@ -547,13 +1020,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -570,6 +1045,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -583,6 +1059,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -595,6 +1072,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -609,6 +1087,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -619,6 +1098,7 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -632,6 +1112,7 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -645,6 +1126,7 @@ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.15.0" }, @@ -662,19 +1144,22 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -688,15 +1173,17 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -704,11 +1191,25 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -725,6 +1226,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -742,6 +1244,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -757,6 +1260,7 @@ "engines": [ "node >= 0.8.0" ], + "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } @@ -766,6 +1270,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -775,6 +1280,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -787,6 +1293,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -795,29 +1302,77 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "node_modules/baseline-browser-mapping": { + "version": "2.10.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", + "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -826,34 +1381,52 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "dev": true, + "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bonjour-service": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", - "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" @@ -863,15 +1436,20 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -879,6 +1457,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -887,9 +1466,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -905,11 +1484,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -922,13 +1503,15 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, + "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" }, @@ -944,21 +1527,44 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -972,15 +1578,16 @@ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, + "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001679", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", - "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", "dev": true, "funding": [ { @@ -995,13 +1602,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1016,6 +1625,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1040,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1052,6 +1663,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0" } @@ -1061,6 +1673,7 @@ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, + "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, @@ -1073,6 +1686,7 @@ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -1087,6 +1701,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -1095,25 +1710,29 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } @@ -1123,6 +1742,7 @@ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -1131,16 +1751,17 @@ } }, "node_modules/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -1153,6 +1774,7 @@ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } @@ -1162,6 +1784,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -1174,40 +1797,43 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" }, "node_modules/copy-webpack-plugin": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", - "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz", + "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==", "dev": true, + "license": "MIT", "dependencies": { - "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^14.0.0", "normalize-path": "^3.0.0", "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2" + "serialize-javascript": "^7.0.3", + "tinyglobby": "^0.2.12" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 20.9.0" }, "funding": { "type": "opencollective", @@ -1221,13 +1847,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1242,6 +1870,7 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -1254,10 +1883,11 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -1270,15 +1900,17 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, + "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" @@ -1291,10 +1923,11 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1302,28 +1935,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1336,6 +1953,7 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1345,6 +1963,7 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -1354,13 +1973,15 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, + "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -1373,6 +1994,7 @@ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, + "license": "MIT", "dependencies": { "utila": "~0.4" } @@ -1382,6 +2004,7 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -1401,13 +2024,15 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -1423,6 +2048,7 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -1437,52 +2063,60 @@ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.55", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", - "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -1493,15 +2127,17 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, + "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/envinfo": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", - "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", "dev": true, + "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -1510,13 +2146,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1526,21 +2160,37 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1549,13 +2199,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -1565,6 +2217,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -1578,6 +2231,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1590,6 +2244,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1599,6 +2254,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1608,6 +2264,7 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1616,128 +2273,106 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.9.1" } }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, + "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -1745,11 +2380,30 @@ "node": ">=0.8.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1758,17 +2412,18 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -1780,6 +2435,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -1793,14 +2449,15 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -1808,6 +2465,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1817,27 +2475,12 @@ } } }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1847,6 +2490,7 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1857,6 +2501,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1870,21 +2515,28 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1893,24 +2545,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1921,6 +2582,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1928,39 +2590,38 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=10.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1970,52 +2631,32 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2024,10 +2665,11 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2040,6 +2682,7 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, + "license": "MIT", "bin": { "he": "bin/he" } @@ -2049,6 +2692,7 @@ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -2061,6 +2705,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2075,38 +2720,25 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, - "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ] - }, "node_modules/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, + "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", @@ -2124,10 +2756,11 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", - "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", + "version": "5.6.7", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz", + "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==", "dev": true, + "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", @@ -2167,6 +2800,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", @@ -2178,35 +2812,43 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -2217,10 +2859,11 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -2245,6 +2888,7 @@ "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.18" } @@ -2254,6 +2898,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -2261,20 +2906,12 @@ "node": ">=0.10.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -2293,22 +2930,25 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" } @@ -2318,6 +2958,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -2326,10 +2967,11 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2345,6 +2987,7 @@ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -2360,24 +3003,17 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2390,6 +3026,7 @@ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^3.0.0" }, @@ -2404,10 +3041,11 @@ } }, "node_modules/is-network-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", - "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -2420,6 +3058,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2429,6 +3068,7 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2441,6 +3081,7 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -2449,10 +3090,11 @@ } }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, + "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" }, @@ -2467,43 +3109,32 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jest-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -2518,6 +3149,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2527,6 +3159,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2540,46 +3173,49 @@ "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", "dev": true, + "license": "MIT", "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { @@ -2587,6 +3223,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -2595,55 +3232,80 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, "node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz", - "integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==", + "version": "4.57.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", + "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/json-pack": "^1.0.3", - "@jsonjoy.com/util": "^1.3.0", - "tree-dump": "^1.0.1", + "@jsonjoy.com/fs-core": "4.57.2", + "@jsonjoy.com/fs-fsa": "4.57.2", + "@jsonjoy.com/fs-node": "4.57.2", + "@jsonjoy.com/fs-node-builtins": "4.57.2", + "@jsonjoy.com/fs-node-to-fsa": "4.57.2", + "@jsonjoy.com/fs-node-utils": "4.57.2", + "@jsonjoy.com/fs-print": "4.57.2", + "@jsonjoy.com/fs-snapshot": "4.57.2", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", "tslib": "^2.0.0" }, - "engines": { - "node": ">= 4.0.0" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" } }, "node_modules/merge-descriptors": { @@ -2651,6 +3313,7 @@ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -2659,22 +3322,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "engines": { - "node": ">= 8" - } + "license": "MIT" }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2684,6 +3340,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -2692,11 +3349,25 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -2705,10 +3376,11 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2718,6 +3390,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -2725,32 +3398,45 @@ "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -2759,13 +3445,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, + "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -2779,6 +3467,7 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2787,38 +3476,33 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, + "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2828,6 +3512,7 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -2836,10 +3521,11 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2851,13 +3537,15 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -2866,24 +3554,26 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, + "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -2897,6 +3587,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -2912,6 +3603,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -2920,10 +3612,11 @@ } }, "node_modules/p-retry": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", - "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", @@ -2941,6 +3634,7 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2949,13 +3643,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, + "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -2966,6 +3662,7 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2975,6 +3672,7 @@ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, + "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -2985,6 +3683,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2994,6 +3693,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3002,55 +3702,48 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true - }, - "node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3061,6 +3754,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -3068,11 +3762,30 @@ "node": ">=8" } }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" @@ -3082,13 +3795,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -3102,26 +3817,39 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=16.0.0" } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3130,54 +3858,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -3188,6 +3889,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3202,6 +3904,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -3209,11 +3912,25 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, @@ -3221,11 +3938,19 @@ "node": ">= 10.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -3235,6 +3960,7 @@ "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", "dev": true, + "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", @@ -3248,6 +3974,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3256,21 +3983,27 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3280,6 +4013,7 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -3292,6 +4026,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3301,28 +4036,20 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" @@ -3335,10 +4062,11 @@ } }, "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -3346,29 +4074,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3387,19 +4092,22 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -3407,7 +4115,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -3418,85 +4126,86 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, - "dependencies": { - "randombytes": "^2.1.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" } }, "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", "dev": true, + "license": "MIT", "dependencies": { - "accepts": "~1.3.4", + "accepts": "~1.3.8", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-index/node_modules/depd": { @@ -3504,89 +4213,67 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, + "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "dev": true, + "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, + "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -3599,6 +4286,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3611,29 +4299,36 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3642,28 +4337,60 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, "engines": { - "node": ">=14.16" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sockjs": { @@ -3671,6 +4398,7 @@ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, + "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", @@ -3682,6 +4410,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -3691,6 +4420,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3701,6 +4431,7 @@ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", @@ -3717,6 +4448,7 @@ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", @@ -3727,10 +4459,11 @@ } }, "node_modules/spdy-transport/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3747,13 +4480,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/spdy/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -3770,13 +4505,15 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3786,93 +4523,17 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3885,6 +4546,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -3897,6 +4559,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3905,22 +4568,28 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", + "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -3932,16 +4601,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -3965,69 +4634,26 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/thingies": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", - "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.18" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, "peerDependencies": { "tslib": "^2" } @@ -4036,13 +4662,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -4055,15 +4700,17 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/tree-dump": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", - "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.0" }, @@ -4079,13 +4726,35 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "dev": true, + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -4095,36 +4764,26 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -4140,9 +4799,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -4151,32 +4811,26 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/utila": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -4186,6 +4840,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -4195,15 +4850,17 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -4217,39 +4874,42 @@ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, + "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -4272,6 +4932,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -4317,19 +4978,21 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" } }, "node_modules/webpack-dev-middleware": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", - "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", "dev": true, + "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^2.1.31", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" @@ -4350,15 +5013,34 @@ } } }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/webpack-dev-server": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", - "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", + "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", "@types/sockjs": "^0.3.36", @@ -4367,18 +5049,17 @@ "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", - "compression": "^1.7.4", + "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", - "express": "^4.19.2", + "express": "^4.22.1", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", + "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", @@ -4412,6 +5093,7 @@ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", "dev": true, + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", @@ -4422,68 +5104,21 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz", + "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -4498,6 +5133,7 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } @@ -4507,6 +5143,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -4521,140 +5158,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "license": "MIT" }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -4670,6 +5182,22 @@ "optional": true } } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/web/package.json b/web/package.json index 83f9eda1..e68c5377 100644 --- a/web/package.json +++ b/web/package.json @@ -38,11 +38,11 @@ }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^1.7.0", - "copy-webpack-plugin": "^12.0.2", + "copy-webpack-plugin": "^14.0.0", "html-webpack-plugin": "^5.6.3", + "rimraf": "^6.0.1", "webpack": "^5.96.1", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.1.0", - "rimraf": "^6.0.1" + "webpack-dev-server": "^5.1.0" } } From 2d5a6672ac2d5c9d13d751d5f6f40cb1d4a2a318 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 18:00:00 -0700 Subject: [PATCH 090/110] Replace wasm-pack plugin with wasm-bindgen builds Build the web package with explicit cargo and wasm-bindgen commands before invoking webpack. Remove the unmaintained webpack plugin, install cargo-binstall in deploy workflows, and use it to install wasm-bindgen-cli while keeping wasm-pack for the existing test command. --- .github/workflows/.publish.sh.swp | Bin 0 -> 12288 bytes .github/workflows/deploy-release.yml | 3 + .github/workflows/deploy-staging.yml | 3 + .github/workflows/publish.sh | 3 + web/package-lock.json | 99 --------------------------- web/package.json | 7 +- web/webpack.config.js | 6 -- 7 files changed, 13 insertions(+), 108 deletions(-) create mode 100644 .github/workflows/.publish.sh.swp diff --git a/.github/workflows/.publish.sh.swp b/.github/workflows/.publish.sh.swp new file mode 100644 index 0000000000000000000000000000000000000000..404500a6d38e05480366f3abbda9722ad9f19bb9 GIT binary patch literal 12288 zcmeI2O>7%Q7>1`DkoZZ9;DCV3*G*_zk==D%gdkNACP`I8<4AUrs-h6BcgOZLyEDto ztQ{)}T*@8p&?DU73K9o6a6#O-!4+{p9C`t9X$(+feB;d+FznJ0f1$AfFEdHpGNHOu;C_di{Gi!zcb$hcO$KyrJh`EQj5LuEoP zlXhf890pxsdqq1OD81WB)xd;#(N2Zw1*Uf=E3V%Wa0Kp}Kw&%n^E2#hb+J;eo%K%A zV~=j#wHdd-5pV!QQwYZGv0K{lqiY-iN5Bzq z1RMcJz!7i+905nb5pVh6amIdtu0u`ee(3WV#y)~Rgg$`Yhu(p%K|?5jeCP!9{lkoX3w;TF27L-?s1E)5 z5M!S}A4Bg#SD;PkJoF;83_S-u4gH3=zk|Ml${cd&J;+^-fFs}tI0BAkGkNo<*DgewEY0oz z+xDbN_zsu-L+=*nv6$+B*8y)cW;kLj_L%HvR2YsKsWqeCXVCi^`G&l?{l@nC&T74R z7-AX@wtDnBojOIq?S>fN%ayh?Ub2kU7dF<{4uP63H@F$fYE7=h` zBX1Cy%*R;L<oU|-1bV@yIbF&nN<*IVt z_#ki~R5)^;NAYer&XQ}~2@7)I+3LW1c{q3){hv7gWBi2ckI{%4Vj$Bq9bBTBiSvYE~l=-u1g`ilyjX4Qx<@sNG6nuOxVbl z&#-i<)@;@qoAtF8Z8T|Rqp@1wtZy{%be?LB%k*l!vARGU$F&g!id1uB2wp{&r-CN| zQH%33JlHDVc`e$7<;h>eCSh)xpd_Lpuhyb^%cGZTt$J&LF4Z?LY;0}9)|O05Pny43 z2n~DfAw@Y{#!)-vNOMpI5ut7|W)%rZ11&7_T_B@6s4b0%U}v&(sJX#l5JV`j9uJi6 zhOkxQG~vbi%37nf=2wHtXo>1f+a7Z02>9oFF~_$GUa{|pDv~j$0U%AU z1e?pMpe!1aYTF2tBTUbSur}VmX-u_IWRsBv+CF+|2~pAMa{ImUBJa#O8jkHsWk(^e nz^oSCo~3G~TA^2pR47{KT1W+NYceVMuCUpY6yl>0q6~irYqlfh literal 0 HcmV?d00001 diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 6e9aca2b..341bb966 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -25,6 +25,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: cargo-bins/cargo-binstall@main + with: + version: "1.18.1" - run: ./.github/workflows/publish.sh prod - uses: JamesIves/github-pages-deploy-action@4.1.4 with: diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index fb0335aa..1645793b 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -28,6 +28,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: cargo-bins/cargo-binstall@main + with: + version: "1.18.1" - run: ./.github/workflows/publish.sh staging - uses: JamesIves/github-pages-deploy-action@4.1.4 with: diff --git a/.github/workflows/publish.sh b/.github/workflows/publish.sh index 27bf0472..766215d4 100755 --- a/.github/workflows/publish.sh +++ b/.github/workflows/publish.sh @@ -28,6 +28,9 @@ nvm install --lts set -ux curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +rustup target add wasm32-unknown-unknown +wasm_bindgen_version="$(grep '^wasm-bindgen = ' web/Cargo.toml | cut -d '"' -f 2)" +cargo binstall --no-confirm "wasm-bindgen-cli@${wasm_bindgen_version}" export NVM_DIR="${HOME}/.nvm" [ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh" diff --git a/web/package-lock.json b/web/package-lock.json index aef82d52..3dc995b0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,6 @@ "jquery": "3.7.1" }, "devDependencies": { - "@wasm-tool/wasm-pack-plugin": "^1.7.0", "copy-webpack-plugin": "^14.0.0", "html-webpack-plugin": "^5.6.3", "rimraf": "^6.0.1", @@ -919,19 +918,6 @@ "@types/node": "*" } }, - "node_modules/@wasm-tool/wasm-pack-plugin": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@wasm-tool/wasm-pack-plugin/-/wasm-pack-plugin-1.7.0.tgz", - "integrity": "sha512-WikzYsw7nTd5CZxH75h7NxM/FLJAgqfWt+/gk3EL3wYKxiIlpMIYPja+sHQl3ARiicIYy4BDfxkbAVjRYlouTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.4.1", - "command-exists": "^1.2.7", - "watchpack": "^2.1.1", - "which": "^2.0.2" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -1275,19 +1261,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1605,21 +1578,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1696,23 +1654,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -1720,13 +1661,6 @@ "dev": true, "license": "MIT" }, - "node_modules/command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -2202,16 +2136,6 @@ "dev": true, "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -2641,16 +2565,6 @@ "dev": true, "license": "MIT" }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -4541,19 +4455,6 @@ "node": ">=8" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", diff --git a/web/package.json b/web/package.json index e68c5377..68d6ba15 100644 --- a/web/package.json +++ b/web/package.json @@ -4,8 +4,10 @@ "description": "The EndBASIC programming language - web interface", "private": true, "scripts": { - "_build:release": "webpack --mode=production", - "_build:debug": "webpack --mode=development", + "_wasm:build:release": "cargo build --target wasm32-unknown-unknown --release && wasm-bindgen --target bundler --out-dir pkg --out-name index ../target/wasm32-unknown-unknown/release/endbasic_web.wasm", + "_wasm:build:debug": "cargo build --target wasm32-unknown-unknown && wasm-bindgen --target bundler --out-dir pkg --out-name index ../target/wasm32-unknown-unknown/debug/endbasic_web.wasm", + "_build:release": "npm run _wasm:build:release && webpack --mode=production", + "_build:debug": "npm run _wasm:build:debug && webpack --mode=development", "build:prod": "NODE_ENV=prod npm run _build:release", "build:staging": "NODE_ENV=staging npm run _build:release", "build:local": "NODE_ENV=local npm run _build:debug", @@ -37,7 +39,6 @@ "jquery": "3.7.1" }, "devDependencies": { - "@wasm-tool/wasm-pack-plugin": "^1.7.0", "copy-webpack-plugin": "^14.0.0", "html-webpack-plugin": "^5.6.3", "rimraf": "^6.0.1", diff --git a/web/webpack.config.js b/web/webpack.config.js index 51ec00a4..b6003076 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -1,7 +1,6 @@ const path = require("path"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); const { DefinePlugin } = require("webpack"); const distDir = path.resolve(__dirname, "dist"); @@ -68,11 +67,6 @@ module.exports = { template: 'src/index.html', }), - new WasmPackPlugin({ - crateDirectory: path.resolve(__dirname), - outDir: path.join(__dirname, "pkg"), - }), - new DefinePlugin({ __SERVICE_URL__: getServiceUrl() }) From 7aa7f11ddaf8d989609e17607dceccf1e263a17b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 18:05:14 -0700 Subject: [PATCH 091/110] Replace wasm-pack test with wasm-bindgen runner Run wasm browser tests through cargo using wasm-bindgen-test-runner and a pinned geckodriver binary from npm. Drop the wasm-pack installer from publish workflows now that the test path no longer depends on wasm-pack. --- .github/workflows/publish.sh | 1 - web/package-lock.json | 259 +++++++++++++++++++++++++++++++++++ web/package.json | 4 +- 3 files changed, 262 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.sh b/.github/workflows/publish.sh index 766215d4..8e2c5eae 100755 --- a/.github/workflows/publish.sh +++ b/.github/workflows/publish.sh @@ -27,7 +27,6 @@ set +ux nvm install --lts set -ux -curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh rustup target add wasm32-unknown-unknown wasm_bindgen_version="$(grep '^wasm-bindgen = ' web/Cargo.toml | cut -d '"' -f 2)" cargo binstall --no-confirm "wasm-bindgen-cli@${wasm_bindgen_version}" diff --git a/web/package-lock.json b/web/package-lock.json index 3dc995b0..97efde1b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "copy-webpack-plugin": "^14.0.0", + "geckodriver": "^6.1.0", "html-webpack-plugin": "^5.6.3", "rimraf": "^6.0.1", "webpack": "^5.96.1", @@ -918,6 +919,52 @@ "@types/node": "*" } }, + "node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@wdio/logger/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -1140,6 +1187,18 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.26", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz", + "integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1190,6 +1249,16 @@ "acorn": "^8.14.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1578,6 +1647,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1839,6 +1921,19 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", @@ -2444,6 +2539,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/geckodriver": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz", + "integrity": "sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "modern-tar": "^0.7.2" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2772,6 +2889,45 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-middleware": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", @@ -2797,6 +2953,45 @@ } } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -3152,6 +3347,27 @@ "dev": true, "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true, + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -3355,6 +3571,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3945,6 +4171,16 @@ "node": ">=8" } }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -4009,6 +4245,29 @@ ], "license": "MIT" }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/web/package.json b/web/package.json index 68d6ba15..f97d76b6 100644 --- a/web/package.json +++ b/web/package.json @@ -16,7 +16,8 @@ "start:prod": "NODE_ENV=prod npm run _start:release", "start:staging": "NODE_ENV=staging npm run _start:release", "start:local": "NODE_ENV=local npm run _start:debug", - "test": "cargo test && wasm-pack test --headless --firefox", + "test:wasm": "GECKODRIVER=./node_modules/.bin/geckodriver CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner cargo test --target wasm32-unknown-unknown", + "test": "cargo test && npm run test:wasm", "clean": "rimraf dist pkg" }, "repository": { @@ -40,6 +41,7 @@ }, "devDependencies": { "copy-webpack-plugin": "^14.0.0", + "geckodriver": "^6.1.0", "html-webpack-plugin": "^5.6.3", "rimraf": "^6.0.1", "webpack": "^5.96.1", From 0e2688415d409654127259faaa63d53977b83679 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 18:10:42 -0700 Subject: [PATCH 092/110] Keep timeout callback alive in wasm tests Retain ownership of the JavaScript timeout closure while awaiting its completion so the receiver does not see a closed channel. This stabilizes wasm-bindgen-test-runner execution in headless Firefox. --- web/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib.rs b/web/src/lib.rs index e691fc7d..c4a499c5 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -84,7 +84,7 @@ fn do_sleep(ms: i32, ret: T) -> Pin>> { } Box::pin(async move { - let _ = callback; // Must grab ownership so that the closure remains alive until it is used. + let _callback = callback; // Must grab ownership so that the closure remains alive until it is used. if let Err(e) = timeout_rx.recv().await { log_and_panic!("Failed to wait for timeout: {}", e); } From b174132552978d013fa120c83e36d3973ca9f661 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 18:15:38 -0700 Subject: [PATCH 093/110] Allow pushing to staging via a tag This is to let me push dev branches to staging as one-offs before merging the changes into master. --- .github/workflows/deploy-staging.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 1645793b..257406b2 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -20,6 +20,8 @@ on: branches: - master - release + tags: + - staging-push schedule: - cron: '0 0 * * *' # Daily. From babf4385a055ee57e742554e642ef20e27b785df Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 24 Apr 2026 22:52:54 -0700 Subject: [PATCH 094/110] Refactor tests for examples In order to test the examples, we need to load each example into the interpreter, run it, and perform some post-validation steps. Right now, this is done by passing a script to the interpreter in interactive mode so that the script can use program-level commands such as LOAD and RUN. Unfortunately, with the core rewrite I'm working on, the execution model of EndBASIC changes and these commands *replace* the running program image. If we have pre-compiled the example wrapping script, as soon as we do a LOAD of the example itself, the wrapper code is lost. To future-proof these tests, change them so that all wrapping code is fed to the interpreter as if it were REPL input. This way, the REPL interprets each line individually and can see any commands that follow LOAD and RUN. --- cli/tests/examples/fibonacci.bas | 21 --------------------- cli/tests/examples/fibonacci.in | 3 +++ cli/tests/examples/fibonacci.out | 7 +++++++ cli/tests/examples/gpio.bas | 29 ----------------------------- cli/tests/examples/gpio.in | 9 +++++++++ cli/tests/examples/gpio.out | 7 +++++++ cli/tests/examples/guess.bas | 22 ---------------------- cli/tests/examples/guess.in | 4 ++++ cli/tests/examples/guess.out | 7 +++++++ cli/tests/examples/hello.bas | 21 --------------------- cli/tests/examples/hello.in | 3 +++ cli/tests/examples/hello.out | 7 +++++++ cli/tests/examples/palette.bas | 21 --------------------- cli/tests/examples/palette.in | 3 +++ cli/tests/examples/palette.out | 7 +++++++ cli/tests/examples/tour.bas | 21 --------------------- cli/tests/examples/tour.in | 3 +++ cli/tests/examples/tour.out | 7 +++++++ cli/tests/integration_test.rs | 21 ++++++++------------- cli/tests/repl/regen.sh | 31 ++++++++++++++++--------------- 20 files changed, 91 insertions(+), 163 deletions(-) delete mode 100644 cli/tests/examples/fibonacci.bas create mode 100644 cli/tests/examples/fibonacci.in delete mode 100644 cli/tests/examples/gpio.bas delete mode 100644 cli/tests/examples/guess.bas delete mode 100644 cli/tests/examples/hello.bas delete mode 100644 cli/tests/examples/palette.bas create mode 100644 cli/tests/examples/palette.in delete mode 100644 cli/tests/examples/tour.bas diff --git a/cli/tests/examples/fibonacci.bas b/cli/tests/examples/fibonacci.bas deleted file mode 100644 index 8b2e660c..00000000 --- a/cli/tests/examples/fibonacci.bas +++ /dev/null @@ -1,21 +0,0 @@ -' EndBASIC -' Copyright 2024 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. - -' Runs the hello.bas example as a demo. - -LOAD "DEMOS:/FIBONACCI.BAS" -RUN - -DISASM diff --git a/cli/tests/examples/fibonacci.in b/cli/tests/examples/fibonacci.in new file mode 100644 index 00000000..100e9515 --- /dev/null +++ b/cli/tests/examples/fibonacci.in @@ -0,0 +1,3 @@ +LOAD "DEMOS:/FIBONACCI.BAS" +RUN +DISASM diff --git a/cli/tests/examples/fibonacci.out b/cli/tests/examples/fibonacci.out index 3af465b3..1ddb84a4 100644 --- a/cli/tests/examples/fibonacci.out +++ b/cli/tests/examples/fibonacci.out @@ -1,3 +1,9 @@ + + EndBASIC X.Y.Z + Copyright YYYY-YYYY Julio Merino + + Type HELP for interactive usage information. + fibonacci of 10 is: 55 took 177 steps to calculate 0000 DIMSV% STEPS @@ -56,3 +62,4 @@ took 177 steps to calculate 0035 LEAVE 0036 RET # 30:1 +End of input by CTRL-D diff --git a/cli/tests/examples/gpio.bas b/cli/tests/examples/gpio.bas deleted file mode 100644 index 91dbf212..00000000 --- a/cli/tests/examples/gpio.bas +++ /dev/null @@ -1,29 +0,0 @@ -' 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. - -' Runs the gpio.bas example using mock GPIO data to drive the program. - -LOAD "DEMOS:/GPIO.BAS" - -GPIO_MOCK_INJECT 8, TRUE -GPIO_MOCK_INJECT 8, TRUE -GPIO_MOCK_INJECT 8, FALSE - -RUN - -PRINT "Dumping GPIO trace..." -PRINT GPIO_MOCK_TRACE$ - -DISASM diff --git a/cli/tests/examples/gpio.in b/cli/tests/examples/gpio.in index 8b137891..639d17f9 100644 --- a/cli/tests/examples/gpio.in +++ b/cli/tests/examples/gpio.in @@ -1 +1,10 @@ +LOAD "DEMOS:/GPIO.BAS" +GPIO_MOCK_INJECT 8, TRUE +GPIO_MOCK_INJECT 8, TRUE +GPIO_MOCK_INJECT 8, FALSE +RUN + +PRINT "Dumping GPIO trace..." +PRINT GPIO_MOCK_TRACE$ +DISASM diff --git a/cli/tests/examples/gpio.out b/cli/tests/examples/gpio.out index ade18097..1cd4e78b 100644 --- a/cli/tests/examples/gpio.out +++ b/cli/tests/examples/gpio.out @@ -1,3 +1,9 @@ + + EndBASIC X.Y.Z + Copyright YYYY-YYYY Julio Merino + + Type HELP for interactive usage information. +  GPIO demo =========== @@ -175,3 +181,4 @@ Dumping GPIO trace... 008d SETV I 008e JMP 0079 +End of input by CTRL-D diff --git a/cli/tests/examples/guess.bas b/cli/tests/examples/guess.bas deleted file mode 100644 index 214750f8..00000000 --- a/cli/tests/examples/guess.bas +++ /dev/null @@ -1,22 +0,0 @@ -' 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. - -' Runs the guess.bas example as a demo. - -LOAD "DEMOS:/GUESS.BAS" -RANDOMIZE 10 -RUN - -DISASM diff --git a/cli/tests/examples/guess.in b/cli/tests/examples/guess.in index c0b74998..9b8058dc 100644 --- a/cli/tests/examples/guess.in +++ b/cli/tests/examples/guess.in @@ -1,3 +1,6 @@ +LOAD "DEMOS:/GUESS.BAS" +RANDOMIZE 10 +RUN 100 3 10 @@ -11,3 +14,4 @@ yes 50 82 no +DISASM diff --git a/cli/tests/examples/guess.out b/cli/tests/examples/guess.out index afc36a90..c27752b4 100644 --- a/cli/tests/examples/guess.out +++ b/cli/tests/examples/guess.out @@ -1,3 +1,9 @@ + + EndBASIC X.Y.Z + Copyright YYYY-YYYY Julio Merino + + Type HELP for interactive usage information. +   Guess the number! =================== @@ -248,3 +254,4 @@ Thanks for playing 00cc CALLB 48 (PRINT), 2 # 79:1 00cd CALLB 48 (PRINT), 0 # 80:1 +End of input by CTRL-D diff --git a/cli/tests/examples/hello.bas b/cli/tests/examples/hello.bas deleted file mode 100644 index ef1e4c6a..00000000 --- a/cli/tests/examples/hello.bas +++ /dev/null @@ -1,21 +0,0 @@ -' 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. - -' Runs the hello.bas example as a demo. - -LOAD "DEMOS:/HELLO.BAS" -RUN - -DISASM diff --git a/cli/tests/examples/hello.in b/cli/tests/examples/hello.in index 9ad35ee3..d9a474f2 100644 --- a/cli/tests/examples/hello.in +++ b/cli/tests/examples/hello.in @@ -1 +1,4 @@ +LOAD "DEMOS:/HELLO.BAS" +RUN First-Name Last-Name +DISASM diff --git a/cli/tests/examples/hello.out b/cli/tests/examples/hello.out index 5bc6bf38..b4302316 100644 --- a/cli/tests/examples/hello.out +++ b/cli/tests/examples/hello.out @@ -1,3 +1,9 @@ + + EndBASIC X.Y.Z + Copyright YYYY-YYYY Julio Merino + + Type HELP for interactive usage information. + Hello,First-Name Last-Name! 0000 LOADR NAME # 16:27 0001 PUSH% 1 # 16:25 @@ -13,3 +19,4 @@ Hello,First-Name Last-Name! 000b PUSH% 4 # 17:7 000c CALLB 48 (PRINT), 5 # 17:1 +End of input by CTRL-D diff --git a/cli/tests/examples/palette.bas b/cli/tests/examples/palette.bas deleted file mode 100644 index 7d67231d..00000000 --- a/cli/tests/examples/palette.bas +++ /dev/null @@ -1,21 +0,0 @@ -' EndBASIC -' Copyright 2022 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. - -' Runs the palette.bas example as a demo. - -LOAD "DEMOS:/PALETTE.BAS" -RUN - -DISASM diff --git a/cli/tests/examples/palette.in b/cli/tests/examples/palette.in new file mode 100644 index 00000000..4df5ead5 --- /dev/null +++ b/cli/tests/examples/palette.in @@ -0,0 +1,3 @@ +LOAD "DEMOS:/PALETTE.BAS" +RUN +DISASM diff --git a/cli/tests/examples/palette.out b/cli/tests/examples/palette.out index 2f404470..06415bb7 100644 --- a/cli/tests/examples/palette.out +++ b/cli/tests/examples/palette.out @@ -1,3 +1,9 @@ + + EndBASIC X.Y.Z + Copyright YYYY-YYYY Julio Merino + + Type HELP for interactive usage information. + [?25l 0  1  2  3  4  5  6  7  8  9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99  100  101  102  103  104  105  106  107  108  109  110  111  112  113  114  115  116  117  118  119  120  121  122  123  124  125  126  127  128  129  130  131  132  133  134  135  136  137  138  139  140  141  142  143  144  145  146  147  148  149  150  151  152  153  154  155  156  157  158  159  160  161  162  163  164  165  166  167  168  169  170  171  172  173  174  175  176  177  178  179  180  181  182  183  184  185  186  187  188  189  190  191  192  193  194  195  196  197  198  199  200  201  202  203  204  205  206  207  208  209  210  211  212  213  214  215  216  217  218  219  220  221  222  223  224  225  226  227  228  229  230  231  232  233  234  235  236  237  238  239  240  241  242  243  244  245  246  247  248  249  250  251  252  253  254  255 [?25h 0000 CALLB 6 (CLS), 0 # 20:1 0001 PUSH% 0 # 21:7 @@ -117,3 +123,4 @@ 0073 CALLB 7 (COLOR), 0 # 46:1 0074 CALLB 48 (PRINT), 0 # 47:1 +End of input by CTRL-D diff --git a/cli/tests/examples/tour.bas b/cli/tests/examples/tour.bas deleted file mode 100644 index f130878a..00000000 --- a/cli/tests/examples/tour.bas +++ /dev/null @@ -1,21 +0,0 @@ -' 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. - -' Runs the hello.bas example as a demo. - -LOAD "DEMOS:/TOUR.BAS" -RUN - -DISASM diff --git a/cli/tests/examples/tour.in b/cli/tests/examples/tour.in index 584ba879..af6b1405 100644 --- a/cli/tests/examples/tour.in +++ b/cli/tests/examples/tour.in @@ -1,3 +1,5 @@ +LOAD "DEMOS:/TOUR.BAS" +RUN @@ -8,3 +10,4 @@ +DISASM diff --git a/cli/tests/examples/tour.out b/cli/tests/examples/tour.out index d0d7fefd..801a1cd9 100644 --- a/cli/tests/examples/tour.out +++ b/cli/tests/examples/tour.out @@ -1,3 +1,9 @@ + + EndBASIC X.Y.Z + Copyright YYYY-YYYY Julio Merino + + Type HELP for interactive usage information. + [?25l EndBASIC tour: Welcome! ========================= @@ -589,3 +595,4 @@ you know, to keep me motivated in writing stuff and building this project: 01ae LEAVE 01af RET # 46:1 +End of input by CTRL-D diff --git a/cli/tests/integration_test.rs b/cli/tests/integration_test.rs index 984abb19..3d3d5ed3 100644 --- a/cli/tests/integration_test.rs +++ b/cli/tests/integration_test.rs @@ -348,9 +348,9 @@ fn test_cli_version() { fn test_example_fibonacci() { check( bin_path("endbasic"), - &["--local-drive=memory://", "--interactive", &src_str("cli/tests/examples/fibonacci.bas")], + &["--local-drive=memory://", "--interactive"], 0, - Behavior::Null, + Behavior::File(src_path("cli/tests/examples/fibonacci.in")), Behavior::File(src_path("cli/tests/examples/fibonacci.out")), Behavior::Null, ); @@ -360,12 +360,7 @@ fn test_example_fibonacci() { fn test_example_gpio() { check( bin_path("endbasic"), - &[ - "--gpio-pins=mock", - "--local-drive=memory://", - "--interactive", - &src_str("cli/tests/examples/gpio.bas"), - ], + &["--gpio-pins=mock", "--local-drive=memory://", "--interactive"], 0, Behavior::File(src_path("cli/tests/examples/gpio.in")), Behavior::File(src_path("cli/tests/examples/gpio.out")), @@ -377,7 +372,7 @@ fn test_example_gpio() { fn test_example_guess() { check( bin_path("endbasic"), - &["--local-drive=memory://", "--interactive", &src_str("cli/tests/examples/guess.bas")], + &["--local-drive=memory://", "--interactive"], 0, Behavior::File(src_path("cli/tests/examples/guess.in")), Behavior::File(src_path("cli/tests/examples/guess.out")), @@ -389,7 +384,7 @@ fn test_example_guess() { fn test_example_hello() { check( bin_path("endbasic"), - &["--local-drive=memory://", "--interactive", &src_str("cli/tests/examples/hello.bas")], + &["--local-drive=memory://", "--interactive"], 0, Behavior::File(src_path("cli/tests/examples/hello.in")), Behavior::File(src_path("cli/tests/examples/hello.out")), @@ -401,9 +396,9 @@ fn test_example_hello() { fn test_example_palette() { check( bin_path("endbasic"), - &["--local-drive=memory://", "--interactive", &src_str("cli/tests/examples/palette.bas")], + &["--local-drive=memory://", "--interactive"], 0, - Behavior::Null, + Behavior::File(src_path("cli/tests/examples/palette.in")), Behavior::File(src_path("cli/tests/examples/palette.out")), Behavior::Null, ); @@ -413,7 +408,7 @@ fn test_example_palette() { fn test_example_tour() { check( bin_path("endbasic"), - &["--local-drive=memory://", "--interactive", &src_str("cli/tests/examples/tour.bas")], + &["--local-drive=memory://", "--interactive"], 0, Behavior::File(src_path("cli/tests/examples/tour.in")), Behavior::File(src_path("cli/tests/examples/tour.out")), diff --git a/cli/tests/repl/regen.sh b/cli/tests/repl/regen.sh index acc07363..180eded2 100755 --- a/cli/tests/repl/regen.sh +++ b/cli/tests/repl/regen.sh @@ -1,3 +1,4 @@ +#! /bin/sh # EndBASIC # Copyright 2021 Julio Merino # @@ -19,10 +20,14 @@ tmpdir="$(mktemp -d)" trap "rm -f \"${tmpdir}\"/*; rmdir \"${tmpdir}\"" EXIT run() { + local gpio_pins="${1}"; shift local local_drive="${1}"; shift - LINES=24 COLUMNS=80 cargo run -- --local-drive="${local_drive}" \ - --interactive "${@}" + LINES=24 COLUMNS=80 cargo run -- \ + --gpio-pins="${gpio_pins}" \ + --interactive \ + --local-drive="${local_drive}" \ + "${@}" } date_re="[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]:[0-5][0-9]" @@ -36,32 +41,28 @@ for outfile in "${@}"; do rm -f "${tmpdir}"/* - local_drive= + gpio_pins=noop + local_drive="file://${tmpdir}" case "${outfile}" in *dir.out) local_drive="memory://" ;; - *load-save.out) - cp "$(dirname "${basfile}")/hello.bas" "${tmpdir}" - local_drive="file://${tmpdir}" - ;; - - *storage.out) - local_drive="file://${tmpdir}" + *gpio.out) + gpio_pins=mock ;; - *) - local_drive="file://${tmpdir}" + *load-save.out) + cp "$(dirname "${basfile}")/hello.bas" "${tmpdir}" ;; esac if [ -f "${basfile}" -a -f "${infile}" ]; then - run "${local_drive}" "${basfile}" <"${infile}" >"${outfile}.new" + run "${gpio_pins}" "${local_drive}" "${basfile}" <"${infile}" >"${outfile}.new" elif [ -f "${basfile}" ]; then - run "${local_drive}" "${basfile}" >"${outfile}.new" + run "${gpio_pins}" "${local_drive}" "${basfile}" >"${outfile}.new" elif [ -f "${infile}" ]; then - run "${local_drive}" <"${infile}" >"${outfile}.new" + run "${gpio_pins}" "${local_drive}" <"${infile}" >"${outfile}.new" else echo "No input for ${outfile}?" 1>&2 continue From b0122aa17ee266400de955618c623641e260aed2 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 17:12:17 -0700 Subject: [PATCH 095/110] Fix regen.sh to handle autoexec.out properly --- cli/tests/repl/regen.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/tests/repl/regen.sh b/cli/tests/repl/regen.sh index 180eded2..f660b066 100755 --- a/cli/tests/repl/regen.sh +++ b/cli/tests/repl/regen.sh @@ -44,6 +44,12 @@ for outfile in "${@}"; do gpio_pins=noop local_drive="file://${tmpdir}" case "${outfile}" in + *autoexec.out) + cp "$(dirname "${basfile}")/autoexec.bas" "${tmpdir}/AUTOEXEC.BAS" + basfile="" + infile="$(dirname "${outfile}")/hello.bas" + ;; + *dir.out) local_drive="memory://" ;; From e6c6abb8d3c1d293d72e7e503ba70304c3e14d0c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 10:43:34 -0700 Subject: [PATCH 096/110] Git-ignore vim swp files This is an oversight on my part that had never been an issue until I started using Jujutsu for version control, which auto-adds files and has already resulted in swp files being checked in by mistake a couple of times... --- .github/workflows/.publish.sh.swp | Bin 12288 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .github/workflows/.publish.sh.swp diff --git a/.github/workflows/.publish.sh.swp b/.github/workflows/.publish.sh.swp deleted file mode 100644 index 404500a6d38e05480366f3abbda9722ad9f19bb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2O>7%Q7>1`DkoZZ9;DCV3*G*_zk==D%gdkNACP`I8<4AUrs-h6BcgOZLyEDto ztQ{)}T*@8p&?DU73K9o6a6#O-!4+{p9C`t9X$(+feB;d+FznJ0f1$AfFEdHpGNHOu;C_di{Gi!zcb$hcO$KyrJh`EQj5LuEoP zlXhf890pxsdqq1OD81WB)xd;#(N2Zw1*Uf=E3V%Wa0Kp}Kw&%n^E2#hb+J;eo%K%A zV~=j#wHdd-5pV!QQwYZGv0K{lqiY-iN5Bzq z1RMcJz!7i+905nb5pVh6amIdtu0u`ee(3WV#y)~Rgg$`Yhu(p%K|?5jeCP!9{lkoX3w;TF27L-?s1E)5 z5M!S}A4Bg#SD;PkJoF;83_S-u4gH3=zk|Ml${cd&J;+^-fFs}tI0BAkGkNo<*DgewEY0oz z+xDbN_zsu-L+=*nv6$+B*8y)cW;kLj_L%HvR2YsKsWqeCXVCi^`G&l?{l@nC&T74R z7-AX@wtDnBojOIq?S>fN%ayh?Ub2kU7dF<{4uP63H@F$fYE7=h` zBX1Cy%*R;L<oU|-1bV@yIbF&nN<*IVt z_#ki~R5)^;NAYer&XQ}~2@7)I+3LW1c{q3){hv7gWBi2ckI{%4Vj$Bq9bBTBiSvYE~l=-u1g`ilyjX4Qx<@sNG6nuOxVbl z&#-i<)@;@qoAtF8Z8T|Rqp@1wtZy{%be?LB%k*l!vARGU$F&g!id1uB2wp{&r-CN| zQH%33JlHDVc`e$7<;h>eCSh)xpd_Lpuhyb^%cGZTt$J&LF4Z?LY;0}9)|O05Pny43 z2n~DfAw@Y{#!)-vNOMpI5ut7|W)%rZ11&7_T_B@6s4b0%U}v&(sJX#l5JV`j9uJi6 zhOkxQG~vbi%37nf=2wHtXo>1f+a7Z02>9oFF~_$GUa{|pDv~j$0U%AU z1e?pMpe!1aYTF2tBTUbSur}VmX-u_IWRsBv+CF+|2~pAMa{ImUBJa#O8jkHsWk(^e nz^oSCo~3G~TA^2pR47{KT1W+NYceVMuCUpY6yl>0q6~irYqlfh diff --git a/.gitignore b/.gitignore index aba4b12f..af0dbe0d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target *.orig *.rej +*.swp *~ # Created by setup-sdl.ps1. From 9131981963aa85af3bc3718dceecb13280071c0e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 17:32:10 -0700 Subject: [PATCH 097/110] Use type mismatch errors for boolean guards Prepare for the upcoming core rewrite by making core report guard type errors with the same type-mismatch wording used by the new compiler. This message is better because it tells users exactly which type was found instead of only saying that a boolean condition was required. --- cli/tests/lang/control-flow-errors.out | 14 +++++++------- core/src/compiler/mod.rs | 21 +++++++++------------ core/src/exec.rs | 10 +++++----- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/cli/tests/lang/control-flow-errors.out b/cli/tests/lang/control-flow-errors.out index a2f1a8b9..541b7b01 100644 --- a/cli/tests/lang/control-flow-errors.out +++ b/cli/tests/lang/control-flow-errors.out @@ -4,12 +4,12 @@ Type HELP for interactive usage information. -ERROR: 1:4: IF/ELSEIF requires a boolean condition -ERROR: 1:4: IF/ELSEIF requires a boolean condition +ERROR: 1:4: Expected BOOLEAN but found INTEGER +ERROR: 1:4: Expected BOOLEAN but found STRING ERROR: 1:15: Cannot <= BOOLEAN and BOOLEAN -ERROR: 1:7: WHILE requires a boolean condition -ERROR: 1:16: LOOP requires a boolean condition -ERROR: 1:16: LOOP requires a boolean condition -ERROR: 1:10: DO requires a boolean condition -ERROR: 1:10: DO requires a boolean condition +ERROR: 1:7: Expected BOOLEAN but found INTEGER +ERROR: 1:16: Expected BOOLEAN but found INTEGER +ERROR: 1:16: Expected BOOLEAN but found STRING +ERROR: 1:10: Expected BOOLEAN but found STRING +ERROR: 1:10: Expected BOOLEAN but found INTEGER End of input by CTRL-D diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 083a499c..3c0398f3 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -62,9 +62,6 @@ pub enum Error { #[error("{0}: EXIT {1} outside of {1}")] MisplacedExit(LineCol, &'static str), - #[error("{0}: {1} requires a boolean condition")] - NotABooleanCondition(LineCol, String), - #[error("{0}: {1} is not a command")] NotACommand(LineCol, VarRef), @@ -596,7 +593,7 @@ impl Compiler { DoGuard::PreUntil(guard) => { let start_pc = self.instrs.len(); - self.compile_expr_guard(guard, "DO")?; + self.compile_expr_guard(guard)?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); @@ -605,7 +602,7 @@ impl Compiler { DoGuard::PreWhile(guard) => { let start_pc = self.instrs.len(); - self.compile_expr_guard(guard, "DO")?; + self.compile_expr_guard(guard)?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; end_pc = self.emit(Instruction::Jump(JumpISpan { addr: start_pc })); @@ -615,14 +612,14 @@ impl Compiler { DoGuard::PostUntil(guard) => { let start_pc = self.instrs.len(); self.compile_many(span.body)?; - self.compile_expr_guard(guard, "LOOP")?; + self.compile_expr_guard(guard)?; end_pc = self.emit(Instruction::JumpIfNotTrue(start_pc)); } DoGuard::PostWhile(guard) => { let start_pc = self.instrs.len(); self.compile_many(span.body)?; - self.compile_expr_guard(guard, "LOOP")?; + self.compile_expr_guard(guard)?; end_pc = self.emit(Instruction::JumpIfTrue(start_pc)); } } @@ -706,7 +703,7 @@ impl Compiler { while let Some(branch) = next { let next2 = iter.next(); - self.compile_expr_guard(branch.guard, "IF/ELSEIF")?; + self.compile_expr_guard(branch.guard)?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(branch.body)?; @@ -813,7 +810,7 @@ impl Compiler { self.compile_many(case.body)?; } Some(guard) => { - self.compile_expr_guard(guard, "SELECT")?; + self.compile_expr_guard(guard)?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(case.body)?; @@ -843,7 +840,7 @@ impl Compiler { /// Compiles a `WHILE` loop and appends its instructions to the compilation context. fn compile_while(&mut self, span: WhileSpan) -> Result<()> { let start_pc = self.instrs.len(); - self.compile_expr_guard(span.expr, "WHILE")?; + self.compile_expr_guard(span.expr)?; let jump_pc = self.emit(Instruction::Nop); self.compile_many(span.body)?; @@ -873,11 +870,11 @@ impl Compiler { /// Compiles an expression that guards a conditional statement. Returns an error if the /// expression is invalid or if it does not evaluate to a boolean. - fn compile_expr_guard>(&mut self, guard: Expr, errmsg: S) -> Result<()> { + fn compile_expr_guard(&mut self, guard: Expr) -> Result<()> { let pos = guard.start_pos(); match self.compile_expr(guard)? { ExprType::Boolean => Ok(()), - _ => Err(Error::NotABooleanCondition(pos, errmsg.into())), + etype => Err(Error::TypeMismatch(pos, etype, ExprType::Boolean)), } } diff --git a/core/src/exec.rs b/core/src/exec.rs index a7830c74..8d19de2a 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -2373,7 +2373,7 @@ mod tests { #[test] fn test_do_errors() { - do_simple_error_test("DO WHILE 2\nLOOP", "1:10: DO requires a boolean condition"); + do_simple_error_test("DO WHILE 2\nLOOP", "1:10: Expected BOOLEAN but found INTEGER"); } #[test] @@ -2552,7 +2552,7 @@ mod tests { OUT "no match" END IF "#; - do_error_test(code, &["5"], &[], "5:20: IF/ELSEIF requires a boolean condition"); + do_error_test(code, &["5"], &[], "5:20: Expected BOOLEAN but found STRING"); } #[test] @@ -2565,10 +2565,10 @@ mod tests { do_simple_error_test("IF TRUE\nEND IF\nOUT 3", "1:8: No THEN in IF statement"); do_simple_error_test("IF 2\nEND IF", "1:5: No THEN in IF statement"); - do_simple_error_test("IF 2 THEN\nEND IF", "1:4: IF/ELSEIF requires a boolean condition"); + do_simple_error_test("IF 2 THEN\nEND IF", "1:4: Expected BOOLEAN but found INTEGER"); do_simple_error_test( "IF FALSE THEN\nELSEIF 2 THEN\nEND IF", - "2:8: IF/ELSEIF requires a boolean condition", + "2:8: Expected BOOLEAN but found INTEGER", ); } @@ -3223,7 +3223,7 @@ mod tests { do_simple_error_test("\n\n\nWHILE 2\n", "4:1: WHILE without WEND"); do_simple_error_test("WHILE 3\nEND", "1:1: WHILE without WEND"); do_simple_error_test("WHILE 3\nEND IF", "2:1: END IF without IF"); - do_simple_error_test("WHILE 2\nWEND", "1:7: WHILE requires a boolean condition"); + do_simple_error_test("WHILE 2\nWEND", "1:7: Expected BOOLEAN but found INTEGER"); } #[test] From 9d5f5a41b59e9c8368d2be760a11961aabbb2a59 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 17:33:50 -0700 Subject: [PATCH 098/110] Unify NOT and shift type diagnostics Prepare for the upcoming core rewrite by aligning core type diagnostics for NOT and shift operators with the newer compiler behavior. These messages are better because they consistently report the full type mismatch, including both operands for binary shift operations. --- cli/tests/lang/exprs-errors.out | 2 +- cli/tests/lang/operators.out | 26 +++++++++++++------------- core/src/compiler/exprs.rs | 26 +++++++++----------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/cli/tests/lang/exprs-errors.out b/cli/tests/lang/exprs-errors.out index d96fdbf4..f1a587de 100644 --- a/cli/tests/lang/exprs-errors.out +++ b/cli/tests/lang/exprs-errors.out @@ -10,7 +10,7 @@ ERROR: 1:7: Undefined symbol X ERROR: 1:13: Cannot AND BOOLEAN and INTEGER ERROR: 1:13: Cannot OR BOOLEAN and INTEGER ERROR: 1:13: Cannot XOR BOOLEAN and INTEGER -ERROR: 1:7: Cannot NOT DOUBLE +ERROR: 1:7: Expected INTEGER but found DOUBLE >>> Relational operations ERROR: 1:13: Cannot = BOOLEAN and INTEGER ERROR: 1:13: Cannot <> BOOLEAN and INTEGER diff --git a/cli/tests/lang/operators.out b/cli/tests/lang/operators.out index a52cb961..0251a84d 100644 --- a/cli/tests/lang/operators.out +++ b/cli/tests/lang/operators.out @@ -42,7 +42,7 @@ ERROR: 1:11: Cannot OR DOUBLE and DOUBLE ERROR: 1:11: Cannot XOR DOUBLE and DOUBLE >>> Test bitwise NOT -1 -ERROR: 1:7: Cannot NOT DOUBLE +ERROR: 1:7: Expected INTEGER but found DOUBLE >>> Test bitwise shift left 12 -268435456 @@ -52,12 +52,12 @@ ERROR: 1:7: Cannot NOT DOUBLE 0 0 0 -ERROR: 1:13: Cannot << BOOLEAN -ERROR: 1:13: Cannot << BOOLEAN -ERROR: 1:11: Cannot << DOUBLE -ERROR: 1:13: Cannot << STRING -ERROR: 1:9: Expected INTEGER but found BOOLEAN -ERROR: 1:9: Expected INTEGER but found DOUBLE +ERROR: 1:13: Cannot << BOOLEAN and BOOLEAN +ERROR: 1:13: Cannot << BOOLEAN and INTEGER +ERROR: 1:11: Cannot << DOUBLE and INTEGER +ERROR: 1:13: Cannot << STRING and INTEGER +ERROR: 1:9: Cannot << INTEGER and BOOLEAN +ERROR: 1:9: Cannot << INTEGER and DOUBLE ERROR: 1:9: Number of bits to << (-1) must be positive >>> Test bitwise shift right 3 @@ -72,12 +72,12 @@ ERROR: 1:9: Number of bits to << (-1) must be positive 0 -1 -1 -ERROR: 1:13: Cannot >> BOOLEAN -ERROR: 1:13: Cannot >> BOOLEAN -ERROR: 1:11: Cannot >> DOUBLE -ERROR: 1:13: Cannot >> STRING -ERROR: 1:9: Expected INTEGER but found BOOLEAN -ERROR: 1:9: Expected INTEGER but found DOUBLE +ERROR: 1:13: Cannot >> BOOLEAN and BOOLEAN +ERROR: 1:13: Cannot >> BOOLEAN and INTEGER +ERROR: 1:11: Cannot >> DOUBLE and INTEGER +ERROR: 1:13: Cannot >> STRING and INTEGER +ERROR: 1:9: Cannot >> INTEGER and BOOLEAN +ERROR: 1:9: Cannot >> INTEGER and DOUBLE ERROR: 1:9: Number of bits to >> (-1) must be positive >>> Test = types TRUE diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 1eae2951..70ec0274 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -85,7 +85,7 @@ fn compile_not_op( instrs.push(Instruction::BitwiseNot(span.pos)); Ok(ExprType::Integer) } - _ => Err(Error::UnaryOpTypeError(span.pos, "NOT", expr_type)), + _ => Err(Error::TypeMismatch(span.pos, expr_type, ExprType::Integer)), } } @@ -266,24 +266,16 @@ fn compile_shift_binary_op Instruction>( op_name: &'static str, ) -> Result { let lhs_type = compile_expr(instrs, fixups, symtable, span.lhs, false)?; - match lhs_type { - ExprType::Integer => (), - _ => { - return Err(Error::UnaryOpTypeError(span.pos, op_name, lhs_type)); - } - }; - let rhs_type = compile_expr(instrs, fixups, symtable, span.rhs, false)?; - match rhs_type { - ExprType::Integer => (), - _ => { - return Err(Error::TypeMismatch(span.pos, rhs_type, ExprType::Integer)); + match (lhs_type, rhs_type) { + (ExprType::Integer, ExprType::Integer) => { + instrs.push(make_inst(span.pos)); + Ok(ExprType::Integer) } - }; - - instrs.push(make_inst(span.pos)); - - Ok(ExprType::Integer) + (lhs_type, rhs_type) => { + Err(Error::BinaryOpTypeError(span.pos, op_name, lhs_type, rhs_type)) + } + } } /// Compiles the evaluation of an expression, appends its instructions to the From edbe147c6d2672dfae038a51e65f982c5b886312 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 17:34:55 -0700 Subject: [PATCH 099/110] Use not-a-number diagnostics for unary minus Prepare for the upcoming core rewrite by aligning unary minus type errors with the newer compiler wording. This message is better because it states the semantic issue directly instead of using an operation-specific phrase that varies across code paths. --- cli/tests/lang/exprs-errors.out | 2 +- cli/tests/lang/operators.out | 4 ++-- core/src/compiler/exprs.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/tests/lang/exprs-errors.out b/cli/tests/lang/exprs-errors.out index f1a587de..4c4923d0 100644 --- a/cli/tests/lang/exprs-errors.out +++ b/cli/tests/lang/exprs-errors.out @@ -25,7 +25,7 @@ ERROR: 1:13: Cannot * BOOLEAN and INTEGER ERROR: 1:13: Cannot / BOOLEAN and INTEGER ERROR: 1:13: Cannot MOD BOOLEAN and INTEGER ERROR: 1:13: Cannot ^ BOOLEAN and INTEGER -ERROR: 1:7: Cannot negate BOOLEAN +ERROR: 1:7: BOOLEAN is not a number >>> Operations and variables ERROR: 1:13: Cannot + INTEGER and BOOLEAN >>> Array accesses diff --git a/cli/tests/lang/operators.out b/cli/tests/lang/operators.out index 0251a84d..623ba7c5 100644 --- a/cli/tests/lang/operators.out +++ b/cli/tests/lang/operators.out @@ -250,12 +250,12 @@ ERROR: 1:9: Exponent -3 cannot be negative 1024 ERROR: 1:10: Cannot ^ STRING and STRING >>> Test unary - types -ERROR: 1:7: Cannot negate BOOLEAN +ERROR: 1:7: BOOLEAN is not a number -6.12 5.53 -6 5 ERROR: 1:7: Integer underflow 2147483648 -ERROR: 1:7: Cannot negate STRING +ERROR: 1:7: STRING is not a number End of input by CTRL-D diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 70ec0274..6e8bf40e 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -106,7 +106,7 @@ fn compile_neg_op( instrs.push(Instruction::NegateInteger(span.pos)); Ok(ExprType::Integer) } - _ => Err(Error::UnaryOpTypeError(span.pos, "negate", expr_type)), + _ => Err(Error::NotANumber(span.pos, expr_type)), } } From c02f3d6cf9974faf29bbcb090cb4e58d5a181347 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 17:39:15 -0700 Subject: [PATCH 100/110] Preserve var refs in undefined symbol errors Prepare for the upcoming core rewrite by making compiler undefined symbol messages preserve the source variable reference instead of a canonicalized symbol key. This is better because users now see the same spelling and type annotation they wrote in source code. --- cli/tests/lang/exprs-errors.out | 6 +++--- cli/tests/repl/state-sharing.out | 6 +++--- core/src/compiler/args.rs | 6 +++--- core/src/compiler/exprs.rs | 10 +++++----- core/src/compiler/mod.rs | 19 ++++++++++--------- core/src/exec.rs | 4 ++-- repl/src/lib.rs | 2 +- std/src/arrays.rs | 2 +- std/src/console/cmds.rs | 2 +- std/src/help.rs | 2 +- 10 files changed, 30 insertions(+), 29 deletions(-) diff --git a/cli/tests/lang/exprs-errors.out b/cli/tests/lang/exprs-errors.out index 4c4923d0..ed374290 100644 --- a/cli/tests/lang/exprs-errors.out +++ b/cli/tests/lang/exprs-errors.out @@ -5,7 +5,7 @@ Type HELP for interactive usage information. >>> Symbol references -ERROR: 1:7: Undefined symbol X +ERROR: 1:7: Undefined symbol x >>> Logical operations ERROR: 1:13: Cannot AND BOOLEAN and INTEGER ERROR: 1:13: Cannot OR BOOLEAN and INTEGER @@ -30,8 +30,8 @@ ERROR: 1:7: BOOLEAN is not a number ERROR: 1:13: Cannot + INTEGER and BOOLEAN >>> Array accesses ERROR: 1:7: Incompatible type annotation in x# reference -ERROR: 1:7: Undefined symbol Y -ERROR: 1:7: Undefined symbol A +ERROR: 1:7: Undefined symbol y$ +ERROR: 1:7: Undefined symbol a >>> Simple function calls ERROR: 1:7: Incompatible type annotation in MAX$ reference ERROR: 1:7: Undefined symbol UNKNOWN diff --git a/cli/tests/repl/state-sharing.out b/cli/tests/repl/state-sharing.out index 2fa7f21e..ff7417e8 100644 --- a/cli/tests/repl/state-sharing.out +++ b/cli/tests/repl/state-sharing.out @@ -5,15 +5,15 @@ Type HELP for interactive usage information. 3 -ERROR: 1:7: Undefined symbol A +ERROR: 1:7: Undefined symbol a [?1049h[?25l ESC Exit | | Ln 1, Col 1  [?25hD[?25l ESC Exit | * | Ln 1, Col 2 [?25hI[?25l ESC Exit | * | Ln 1, Col 3 [?25hM[?25l ESC Exit | * | Ln 1, Col 4 [?25h [?25l ESC Exit | * | Ln 1, Col 5 [?25ha[?25l ESC Exit | * | Ln 1, Col 6 [?25h([?25l ESC Exit | * | Ln 1, Col 7 [?25h1[?25l ESC Exit | * | Ln 1, Col 8 [?25h)[?25l ESC Exit | * | Ln 1, Col 9 [?25h [?25l ESC Exit | * | Ln 1, Col 10 [?25hA[?25l ESC Exit | * | Ln 1, Col 11 [?25hS[?25l ESC Exit | * | Ln 1, Col 12 [?25h [?25l ESC Exit | * | Ln 1, Col 13 [?25hI[?25l ESC Exit | * | Ln 1, Col 14 [?25hN[?25l ESC Exit | * | Ln 1, Col 15 [?25hT[?25l ESC Exit | * | Ln 1, Col 16 [?25hE[?25l ESC Exit | * | Ln 1, Col 17 [?25hG[?25l ESC Exit | * | Ln 1, Col 18 [?25hE[?25l ESC Exit | * | Ln 1, Col 19 [?25hR[?25l ESC Exit | * | Ln 1, Col 20 [?25h[?25l ESC Exit | * | Ln 2, Col 1 [?25ha[?25l ESC Exit | * | Ln 2, Col 2 [?25h([?25l ESC Exit | * | Ln 2, Col 3 [?25h0[?25l ESC Exit | * | Ln 2, Col 4 [?25h)[?25l ESC Exit | * | Ln 2, Col 5 [?25h [?25l ESC Exit | * | Ln 2, Col 6 [?25h=[?25l ESC Exit | * | Ln 2, Col 7 [?25h [?25l ESC Exit | * | Ln 2, Col 8 [?25h1[?25l ESC Exit | * | Ln 2, Col 9 [?25h2[?25l ESC Exit | * | Ln 2, Col 10 [?25h3[?25l ESC Exit | * | Ln 2, Col 11 [?25h[?25l ESC Exit | * | Ln 3, Col 1 [?25hP[?25l ESC Exit | * | Ln 3, Col 2 [?25hR[?25l ESC Exit | * | Ln 3, Col 3 [?25hI[?25l ESC Exit | * | Ln 3, Col 4 [?25hN[?25l ESC Exit | * | Ln 3, Col 5 [?25hT[?25l ESC Exit | * | Ln 3, Col 6 [?25h [?25l ESC Exit | * | Ln 3, Col 7 [?25h"[?25l ESC Exit | * | Ln 3, Col 8 [?25ha[?25l ESC Exit | * | Ln 3, Col 9 [?25h([?25l ESC Exit | * | Ln 3, Col 10 [?25h0[?25l ESC Exit | * | Ln 3, Col 11 [?25h)[?25l ESC Exit | * | Ln 3, Col 12 [?25h [?25l ESC Exit | * | Ln 3, Col 13 [?25hi[?25l ESC Exit | * | Ln 3, Col 14 [?25hs[?25l ESC Exit | * | Ln 3, Col 15 [?25h"[?25l ESC Exit | * | Ln 3, Col 16 [?25h;[?25l ESC Exit | * | Ln 3, Col 17 [?25h [?25l ESC Exit | * | Ln 3, Col 18 [?25ha[?25l ESC Exit | * | Ln 3, Col 19 [?25h([?25l ESC Exit | * | Ln 3, Col 20 [?25h0[?25l ESC Exit | * | Ln 3, Col 21 [?25h)[?25l ESC Exit | * | Ln 3, Col 22 [?25h[?25l ESC Exit | * | Ln 4, Col 1 [?25h[?1049la(0) is 123 a(0) is 123 a(0) is 123 a(0) before CLEAR is 123 -ERROR: 1:30: Undefined symbol A +ERROR: 1:30: Undefined symbol a a(0) is 123 a(0) before NEW is 123 Current program has unsaved changes and has never been saved! -ERROR: 1:28: Undefined symbol A +ERROR: 1:28: Undefined symbol a End of input by CTRL-D diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index f475c728..c77cc263 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -343,7 +343,7 @@ fn compile_required_ref( match symtable.get(&key) { None => { if !define_undefined { - return Err(Error::UndefinedSymbol(span.pos, key)); + return Err(Error::UndefinedSymbol(span.pos, span.vref)); } debug_assert!(!require_array); @@ -1216,7 +1216,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(Error::UndefinedSymbol(lc(1, 2), SymbolKey::from("foo"))) + .exp_error(Error::UndefinedSymbol(lc(1, 2), VarRef::new("foo", None))) .check(); } @@ -1455,7 +1455,7 @@ mod compile_tests { sep: ArgSep::End, sep_pos: lc(1, 5), }]) - .exp_error(Error::UndefinedSymbol(lc(1, 2), SymbolKey::from("foo"))) + .exp_error(Error::UndefinedSymbol(lc(1, 2), VarRef::new("foo", None))) .check(); } diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 6e8bf40e..6831243d 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -346,7 +346,7 @@ fn compile_expr_symbol( ) -> Result { let key = SymbolKey::from(&span.vref.name); let (instr, vtype) = match symtable.get(&key) { - None => return Err(Error::UndefinedSymbol(span.pos, key)), + None => return Err(Error::UndefinedSymbol(span.pos, span.vref)), Some(SymbolPrototype::Array(atype, _dims)) => { if allow_varrefs { @@ -410,7 +410,7 @@ fn compile_expr_symbol( let nargs = compile_function_args(md, instrs, fixups, symtable, span.pos, vec![])?; debug_assert_eq!(0, nargs, "Argless compiler must have returned zero arguments"); - fixups.insert(instrs.len(), Fixup::Call(key, span.pos)); + fixups.insert(instrs.len(), Fixup::Call(span.vref.clone(), span.pos)); (Instruction::Nop, etype) } }; @@ -751,7 +751,7 @@ pub(super) fn compile_expr( let span_pos = span.vref_pos; compile_function_args(md, instrs, fixups, symtable, span_pos, span.args)?; instrs.push(Instruction::Nop); - fixups.insert(instrs.len() - 1, Fixup::Call(key, span_pos)); + fixups.insert(instrs.len() - 1, Fixup::Call(span.vref.clone(), span_pos)); Ok(vtype) } @@ -759,7 +759,7 @@ pub(super) fn compile_expr( Err(Error::NotArrayOrFunction(span.vref_pos, key)) } - None => Err(Error::UndefinedSymbol(span.vref_pos, key)), + None => Err(Error::UndefinedSymbol(span.vref_pos, span.vref)), } } } @@ -1280,7 +1280,7 @@ mod tests { #[test] fn test_compile_expr_array_ref_not_defined() { - Tester::default().parse("i = a(4)").compile().expect_err("1:5: Undefined symbol A").check(); + Tester::default().parse("i = a(4)").compile().expect_err("1:5: Undefined symbol a").check(); } #[test] diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 3c0398f3..80f8ec81 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -90,7 +90,7 @@ pub enum Error { UnaryOpTypeError(LineCol, &'static str, ExprType), #[error("{0}: Undefined symbol {1}")] - UndefinedSymbol(LineCol, SymbolKey), + UndefinedSymbol(LineCol, VarRef), #[error("{0}: Unknown label {1}")] UnknownLabel(LineCol, String), @@ -294,7 +294,7 @@ impl SymbolsTable { #[cfg_attr(test, derive(Debug, PartialEq))] enum Fixup { CallAddr(String, LineCol), - Call(SymbolKey, LineCol), + Call(VarRef, LineCol), GotoAddr(String, LineCol), OnErrorGotoAddr(String, LineCol), } @@ -459,7 +459,7 @@ impl Compiler { return Err(Error::IndexNonArray(span.vref_pos, span.vref.name)); } None => { - return Err(Error::UndefinedSymbol(span.vref_pos, key)); + return Err(Error::UndefinedSymbol(span.vref_pos, span.vref.clone())); } }; @@ -910,7 +910,7 @@ impl Compiler { return Err(Error::NotACommand(span.vref_pos, span.vref)); } - None => return Err(Error::UndefinedSymbol(span.vref_pos, key)), + None => return Err(Error::UndefinedSymbol(span.vref_pos, span.vref)), }; let name_pos = span.vref_pos; @@ -931,7 +931,7 @@ impl Compiler { })); } else { let call_pc = self.emit(Instruction::Nop); - self.fixups.insert(call_pc, Fixup::Call(key, span.vref_pos)); + self.fixups.insert(call_pc, Fixup::Call(span.vref, span.vref_pos)); } } @@ -1211,9 +1211,10 @@ impl Compiler { Instruction::Call(JumpISpan { addr }) } - Fixup::Call(name, pos) => { - let Some(addr) = callables.get(&name) else { - return Err(Error::UndefinedSymbol(pos, name)); + Fixup::Call(vref, pos) => { + let key = SymbolKey::from(&vref.name); + let Some(addr) = callables.get(&key) else { + return Err(Error::UndefinedSymbol(pos, vref)); }; Instruction::Call(JumpISpan { addr: *addr }) } @@ -1563,7 +1564,7 @@ mod tests { Tester::default() .parse("a(3) = FALSE") .compile() - .expect_err("1:1: Undefined symbol A") + .expect_err("1:1: Undefined symbol a") .check(); } diff --git a/core/src/exec.rs b/core/src/exec.rs index 8d19de2a..2cfd70e6 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -1967,7 +1967,7 @@ mod tests { #[test] fn test_array_assignment_errors() { - do_simple_error_test("a() = 3\n", "1:1: Undefined symbol A"); + do_simple_error_test("a() = 3\n", "1:1: Undefined symbol a"); do_simple_error_test("a = 3\na(0) = 3\n", "2:1: Cannot index non-array a"); do_simple_error_test( "DIM a(2)\na() = 3\n", @@ -2031,7 +2031,7 @@ mod tests { #[test] fn test_assignment_errors() { do_simple_error_test("a =\n", "1:4: Missing expression in assignment"); - do_simple_error_test("a = b\n", "1:5: Undefined symbol B"); + do_simple_error_test("a = b\n", "1:5: Undefined symbol b"); do_simple_error_test( "a = 3\na = TRUE\n", "2:1: Cannot assign value of type BOOLEAN to variable of type INTEGER", diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 3fee7a7a..f52cd1ec 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -304,7 +304,7 @@ mod tests { tester .run("after = 5") .expect_var("after", 5) - .expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol UNDEF"]) + .expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol undef"]) .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec) .check(); } diff --git a/std/src/arrays.rs b/std/src/arrays.rs index 60df1641..5d841d65 100644 --- a/std/src/arrays.rs +++ b/std/src/arrays.rs @@ -277,7 +277,7 @@ mod tests { Tester::default() .run(format!("result = {}(x)", func)) - .expect_compilation_err("1:17: Undefined symbol X") + .expect_compilation_err("1:17: Undefined symbol x") .check(); Tester::default() diff --git a/std/src/console/cmds.rs b/std/src/console/cmds.rs index 9d75088c..1cdab57d 100644 --- a/std/src/console/cmds.rs +++ b/std/src/console/cmds.rs @@ -858,7 +858,7 @@ mod tests { "1:13: INPUT expected | <[prompt$] <,|;> vref>", "INPUT \"foo\" AS bar", ); - check_stmt_err("1:7: Undefined symbol A", "INPUT a + 1 ; b"); + check_stmt_err("1:7: Undefined symbol a", "INPUT a + 1 ; b"); Tester::default() .run("a = 3: INPUT ; a + 1") .expect_compilation_err("1:16: Requires a reference, not a value") diff --git a/std/src/help.rs b/std/src/help.rs index c3caceec..759091ff 100644 --- a/std/src/help.rs +++ b/std/src/help.rs @@ -931,7 +931,7 @@ This is the first and only topic with just one line. tester().add_callable(DoNothingCommand::new()).add_callable(EmptyFunction::new()); t.run(r#"HELP foo bar"#).expect_err("1:10: Unexpected value in expression").check(); - t.run(r#"HELP foo"#).expect_compilation_err("1:6: Undefined symbol FOO").check(); + t.run(r#"HELP foo"#).expect_compilation_err("1:6: Undefined symbol foo").check(); t.run(r#"HELP "foo", 3"#) .expect_compilation_err("1:1: HELP expected <> | ") From 5513ed34702797f1c83e6a193651e7da9fc263ff Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 26 Apr 2026 07:08:34 -0700 Subject: [PATCH 101/110] Preserve source refs in not-array errors Preserve source variable references in "not an array nor a function" errors so diagnostics reflect exactly what users typed. Keep test expectations in sync across compiler, exec, and CLI integration tests. --- cli/tests/lang/exprs-errors.out | 2 +- cli/tests/lang/types.out | 2 +- core/src/compiler/args.rs | 2 +- core/src/compiler/exprs.rs | 20 ++++++++++---------- core/src/compiler/mod.rs | 2 +- core/src/exec.rs | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/tests/lang/exprs-errors.out b/cli/tests/lang/exprs-errors.out index ed374290..91c9729f 100644 --- a/cli/tests/lang/exprs-errors.out +++ b/cli/tests/lang/exprs-errors.out @@ -36,6 +36,6 @@ ERROR: 1:7: Undefined symbol a ERROR: 1:7: Incompatible type annotation in MAX$ reference ERROR: 1:7: Undefined symbol UNKNOWN >>> Calling of non-functions -ERROR: 1:7: Z is not an array nor a function +ERROR: 1:7: z is not an array nor a function ERROR: 1:7: PRINT is not an array nor a function End of input by CTRL-D diff --git a/cli/tests/lang/types.out b/cli/tests/lang/types.out index 87be56d5..74cf0836 100644 --- a/cli/tests/lang/types.out +++ b/cli/tests/lang/types.out @@ -23,7 +23,7 @@ ERROR: 1:30: Incompatible type annotation in a2# reference 3 ERROR: 1:7: Incompatible type annotation in LEN$ reference >>> Invalid type access -ERROR: 1:14: I is not an array nor a function +ERROR: 1:14: i is not an array nor a function ERROR: 1:7: LEN expected expr$ ERROR: 1:7: PRINT is not an array nor a function >>> Argless function calls diff --git a/core/src/compiler/args.rs b/core/src/compiler/args.rs index c77cc263..993f2f28 100644 --- a/core/src/compiler/args.rs +++ b/core/src/compiler/args.rs @@ -401,7 +401,7 @@ fn compile_required_ref( )); } - Err(Error::NotArrayOrFunction(span.pos, key)) + Err(Error::NotArrayOrFunction(span.pos, span.vref)) } } } diff --git a/core/src/compiler/exprs.rs b/core/src/compiler/exprs.rs index 6831243d..00baf0bd 100644 --- a/core/src/compiler/exprs.rs +++ b/core/src/compiler/exprs.rs @@ -374,7 +374,7 @@ fn compile_expr_symbol( let etype = match md.return_type() { Some(etype) => etype, None => { - return Err(Error::NotArrayOrFunction(span.pos, key)); + return Err(Error::NotArrayOrFunction(span.pos, span.vref)); } }; @@ -400,7 +400,7 @@ fn compile_expr_symbol( let etype = match md.return_type() { Some(etype) => etype, None => { - return Err(Error::NotArrayOrFunction(span.pos, key)); + return Err(Error::NotArrayOrFunction(span.pos, span.vref)); } }; @@ -708,7 +708,7 @@ pub(super) fn compile_expr( let vtype = match md.return_type() { Some(vtype) => vtype, None => { - return Err(Error::NotArrayOrFunction(span.vref_pos, key)); + return Err(Error::NotArrayOrFunction(span.vref_pos, span.vref)); } }; @@ -740,7 +740,7 @@ pub(super) fn compile_expr( let vtype = match md.return_type() { Some(vtype) => vtype, None => { - return Err(Error::NotArrayOrFunction(span.vref_pos, key)); + return Err(Error::NotArrayOrFunction(span.vref_pos, span.vref)); } }; @@ -756,7 +756,7 @@ pub(super) fn compile_expr( } Some(SymbolPrototype::Variable(_)) => { - Err(Error::NotArrayOrFunction(span.vref_pos, key)) + Err(Error::NotArrayOrFunction(span.vref_pos, span.vref)) } None => Err(Error::UndefinedSymbol(span.vref_pos, span.vref)), @@ -895,7 +895,7 @@ mod tests { .define_callable(CallableMetadataBuilder::new("F").with_return_type(ExprType::Integer)) .parse("c f") .compile() - .expect_err("1:3: F is not an array nor a function") + .expect_err("1:3: f is not an array nor a function") .check(); } @@ -1008,7 +1008,7 @@ mod tests { .define_callable(CallableMetadataBuilder::new("C")) .parse("b = c") .compile() - .expect_err("1:5: C is not an array nor a function") + .expect_err("1:5: c is not an array nor a function") .check(); } @@ -1028,7 +1028,7 @@ mod tests { )])) .parse("c c") .compile() - .expect_err("1:3: C is not an array nor a function") + .expect_err("1:3: c is not an array nor a function") .check(); } @@ -1289,7 +1289,7 @@ mod tests { .define("a", SymbolPrototype::Variable(ExprType::Integer)) .parse("i = a(3)") .compile() - .expect_err("1:5: A is not an array nor a function") + .expect_err("1:5: a is not an array nor a function") .check(); } @@ -1351,7 +1351,7 @@ mod tests { Tester::default() .parse("i = 3: j = i()") .compile() - .expect_err("1:12: I is not an array nor a function") + .expect_err("1:12: i is not an array nor a function") .check(); } } diff --git a/core/src/compiler/mod.rs b/core/src/compiler/mod.rs index 80f8ec81..a9c5a7da 100644 --- a/core/src/compiler/mod.rs +++ b/core/src/compiler/mod.rs @@ -75,7 +75,7 @@ pub enum Error { NotAVariable(LineCol, VarRef), #[error("{0}: {1} is not an array nor a function")] - NotArrayOrFunction(LineCol, SymbolKey), + NotArrayOrFunction(LineCol, VarRef), #[error("{0}: {1}")] ParseError(LineCol, String), diff --git a/core/src/exec.rs b/core/src/exec.rs index 2cfd70e6..62b4b467 100644 --- a/core/src/exec.rs +++ b/core/src/exec.rs @@ -3532,7 +3532,7 @@ mod tests { SUB f: OUT "foo": END SUB OUT f "#; - do_error_test(code, &[], &[], "3:17: F is not an array nor a function"); + do_error_test(code, &[], &[], "3:17: f is not an array nor a function"); } #[test] From 5730db21cfeb898dc1f7296207b1864293fcb04d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 17:40:42 -0700 Subject: [PATCH 102/110] Report negate overflow as overflow Prepare for the upcoming core rewrite by aligning integer negation overflow wording with the VM behavior that users will get after the migration. This message is better because negating the minimum integer exceeds the representable positive range, so overflow is the precise term. --- cli/tests/lang/operators.out | 2 +- core/src/value.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/tests/lang/operators.out b/cli/tests/lang/operators.out index 623ba7c5..fa141e3a 100644 --- a/cli/tests/lang/operators.out +++ b/cli/tests/lang/operators.out @@ -255,7 +255,7 @@ ERROR: 1:7: BOOLEAN is not a number 5.53 -6 5 -ERROR: 1:7: Integer underflow +ERROR: 1:7: Integer overflow 2147483648 ERROR: 1:7: STRING is not a number End of input by CTRL-D diff --git a/core/src/value.rs b/core/src/value.rs index d4e3dcda..52ceabdc 100644 --- a/core/src/value.rs +++ b/core/src/value.rs @@ -162,7 +162,7 @@ pub fn pow_integer(lhs: i32, rhs: i32) -> Result { pub fn neg_integer(i: i32) -> Result { match i.checked_neg() { Some(i) => Ok(i), - None => Err(Error::new("Integer underflow".to_owned())), + None => Err(Error::new("Integer overflow".to_owned())), } } @@ -320,5 +320,6 @@ mod tests { fn test_value_neg_integer() { assert_eq!(-6, neg_integer(6).unwrap()); assert_eq!(5, neg_integer(-5).unwrap()); + assert_eq!("Integer overflow", format!("{}", neg_integer(i32::MIN).unwrap_err())); } } From f61ab1e7051cf848fa85bd0fd8a0ba2ab7120d3d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 1 May 2026 22:30:01 -0700 Subject: [PATCH 103/110] Release EndBASIC 0.12.0 --- .github/workflows/deploy-release.yml | 2 +- .github/workflows/update-version.sh | 2 +- NEWS.md | 4 ++-- README.md | 4 ++-- cli/Cargo.toml | 18 +++++++++--------- client/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- repl/Cargo.toml | 6 +++--- rpi/Cargo.toml | 4 ++-- sdl/Cargo.toml | 6 +++--- st7735s/Cargo.toml | 6 +++--- std/Cargo.toml | 4 ++-- terminal/Cargo.toml | 6 +++--- web/Cargo.toml | 10 +++++----- web/package-lock.json | 4 ++-- web/package.json | 2 +- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 341bb966..505a0797 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -18,7 +18,7 @@ name: Deploy to repl.endbasic.dev on: push: tags: - - endbasic-0.11.1 + - endbasic-0.12.0 jobs: publish: diff --git a/.github/workflows/update-version.sh b/.github/workflows/update-version.sh index 33615f4a..27819d98 100755 --- a/.github/workflows/update-version.sh +++ b/.github/workflows/update-version.sh @@ -63,7 +63,7 @@ main() { replace README.md -E "/releases\/tag/s/[0-9]+\\.[0-9]+\\.[0-9]+/${version}/g" replace README.md -E "/released on/s/[0-9]{4}-[0-9]{2}-[0-9]{2}/${date}/g" - replace NEWS.md -E "/Changes in.*X\\.Y\\.Z/s/[X0-9]+\\.[Y0-9]+\\.[Z0-9]+/${version}/g" + replace NEWS.md -E "/Changes in.*[0-9]+\\.[0-9]+\\.99/s/[0-9]+\\.[0-9]+\\.[0-9]+/${version}/g" replace NEWS.md -E "/STILL UNDER DEVELOPMENT/s/^.*$/**Released on ${date}.**/g" replace .github/workflows/deploy-release.yml \ diff --git a/NEWS.md b/NEWS.md index 304313a2..2dc375b2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,9 +10,9 @@ to talk to the cloud service. If you use the web interface, this should not be a problem, but if you use local builds, please try to stay on the latest release for the time being.** -## Changes in version 0.11.99 +## Changes in version 0.12.0 -STILL UNDER DEVELOPMENT; NOT RELEASED YET. +**Released on 2026-05-02.** * Cleaned up internal error handling, which changes how errors returned from commands and functions are displayed to the user. diff --git a/README.md b/README.md index b9c153a5..29bfa053 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ operating systems and platforms, including macOS, Windows, and Linux. EndBASIC is free software under the [Apache 2.0 License](LICENSE). -**The latest version of EndBASIC is 0.11.1 and was released on 2024-09-14.** +**The latest version of EndBASIC is 0.12.0 and was released on 2026-05-02.** ## Quick start on the web @@ -46,7 +46,7 @@ for the cloud service and upload your programs to share them with the world. ## Quick start on your machine Visit the -[release page](https://github.com/endbasic/endbasic/releases/tag/endbasic-0.11.1) +[release page](https://github.com/endbasic/endbasic/releases/tag/endbasic-0.12.0) to download prebuilt binaries. Once downloaded, unpack the archive and run the `endbasic` binary to get started. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c3c5b3a8..9b73119f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -28,38 +28,38 @@ getoptsargs = "0.1" thiserror = "1.0" [dependencies.endbasic-client] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../client" [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-repl] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../repl" [dependencies.endbasic-rpi] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../rpi" optional = true [dependencies.endbasic-sdl] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../sdl" optional = true [dependencies.endbasic-st7735s] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../st7735s" optional = true [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" [dependencies.endbasic-terminal] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../terminal" optional = true diff --git a/client/Cargo.toml b/client/Cargo.toml index 77f32c42..4a31e920 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-client" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -24,11 +24,11 @@ time = { version = "0.3", features = ["std"] } url = "2.2" [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" [dependencies.reqwest] diff --git a/core/Cargo.toml b/core/Cargo.toml index 50d3b1cf..c8c37a8e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-core" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] diff --git a/repl/Cargo.toml b/repl/Cargo.toml index 3af843d6..0328996c 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-repl" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -19,11 +19,11 @@ async-trait = "0.1" time = { version = "0.3", features = ["std"] } [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" [dev-dependencies] diff --git a/rpi/Cargo.toml b/rpi/Cargo.toml index 36bba0bf..dc854ee2 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-rpi" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -18,5 +18,5 @@ workspace = true rppal = "0.17" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" diff --git a/sdl/Cargo.toml b/sdl/Cargo.toml index 44dcd12e..e92670ac 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-sdl" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -21,11 +21,11 @@ once_cell = "1.8" tempfile = "3" [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" [dependencies.sdl2] diff --git a/st7735s/Cargo.toml b/st7735s/Cargo.toml index 446b3f28..b6639a46 100644 --- a/st7735s/Cargo.toml +++ b/st7735s/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-st7735s" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -20,11 +20,11 @@ async-trait = "0.1" tokio = { version = "1", features = ["full"] } [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" [dev-dependencies] diff --git a/std/Cargo.toml b/std/Cargo.toml index b5f6d61c..2b64e191 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-std" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -23,7 +23,7 @@ thiserror = "1.0" time = { version = "0.3", features = ["formatting", "local-offset", "std"] } [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" # We don't directly use getrandom but rand does, and we have to customize how diff --git a/terminal/Cargo.toml b/terminal/Cargo.toml index 0bbac725..b569bdbf 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-terminal" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -21,10 +21,10 @@ crossterm = "0.27" tokio = { version = "1", features = ["rt"] } [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" diff --git a/web/Cargo.toml b/web/Cargo.toml index 83552cc5..91a8d453 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-web" -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -35,19 +35,19 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" [dependencies.endbasic-client] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../client" [dependencies.endbasic-core] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-repl] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../repl" [dependencies.endbasic-std] -version = "0.11.99" # ENDBASIC-VERSION +version = "0.12.0" # ENDBASIC-VERSION path = "../std" [dependencies.web-sys] diff --git a/web/package-lock.json b/web/package-lock.json index 97efde1b..a0af91bc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "endbasic-www", - "version": "0.11.99", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "endbasic-www", - "version": "0.11.99", + "version": "0.12.0", "license": "Apache-2.0", "dependencies": { "jquery": "3.7.1" diff --git a/web/package.json b/web/package.json index f97d76b6..797b8d7d 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "endbasic-www", - "version": "0.11.99", + "version": "0.12.0", "description": "The EndBASIC programming language - web interface", "private": true, "scripts": { From 4ca0aaa7dcaaab0f646c3de0094870ab2068a66c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 25 Apr 2026 16:48:08 -0700 Subject: [PATCH 104/110] Post-release version bump to 0.12.1 --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 2dc375b2..8661578e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,12 @@ to talk to the cloud service. If you use the web interface, this should not be a problem, but if you use local builds, please try to stay on the latest release for the time being.** +## Changes in version 0.12.1 + +STILL UNDER DEVELOPMENT; NOT RELEASED YET. + +* No changes recorded. + ## Changes in version 0.12.0 **Released on 2026-05-02.** From 8f32bf7fd249273e173cf32dd087ec617978090e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 2 May 2026 21:11:10 +0200 Subject: [PATCH 105/110] Check in Cargo.lock and pin the Rust version Let's start providing reproducible builds by persisting the versions of the dependencies used at any given time and by pinning the Rust version too via rust-toolchain.toml. The former assures guarantees for packagers and the latter helps prevent spurious CI breakages when new Rust versions show up (which happens all the time due to new Clippy warnings). Fixes #278. --- .github/workflows/test.yml | 7 +- .gitignore | 1 - Cargo.lock | 2464 ++++++++++++++++++++++++++++++++++++ NEWS.md | 3 +- rust-toolchain.toml | 2 + 5 files changed, 2469 insertions(+), 8 deletions(-) create mode 100644 Cargo.lock create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e3726e8..9be082ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,15 +21,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - # Use the latest stable Rust version for lint checks to - # verify any new Clippy warnings that may appear. - toolchain: stable - components: clippy, rustfmt - uses: actions/checkout@v6 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev + - run: rustup component add clippy rustfmt - run: ./.github/workflows/lint.sh linux-test: diff --git a/.gitignore b/.gitignore index af0dbe0d..fa3a4445 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -Cargo.lock config.mk target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..ddf577eb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2464 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endbasic" +version = "0.12.0" +dependencies = [ + "anyhow", + "async-channel", + "dirs", + "endbasic-client", + "endbasic-core", + "endbasic-repl", + "endbasic-rpi", + "endbasic-sdl", + "endbasic-st7735s", + "endbasic-std", + "endbasic-terminal", + "getoptsargs", + "regex", + "tempfile", + "thiserror", + "tokio", +] + +[[package]] +name = "endbasic-client" +version = "0.12.0" +dependencies = [ + "async-trait", + "base64", + "bytes", + "endbasic-core", + "endbasic-std", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_test", + "time", + "tokio", + "url", +] + +[[package]] +name = "endbasic-core" +version = "0.12.0" +dependencies = [ + "async-channel", + "async-trait", + "futures-lite", + "thiserror", + "tokio", +] + +[[package]] +name = "endbasic-repl" +version = "0.12.0" +dependencies = [ + "async-trait", + "endbasic-core", + "endbasic-std", + "futures-lite", + "time", +] + +[[package]] +name = "endbasic-rpi" +version = "0.12.0" +dependencies = [ + "endbasic-std", + "rppal", +] + +[[package]] +name = "endbasic-sdl" +version = "0.12.0" +dependencies = [ + "async-channel", + "async-trait", + "endbasic-core", + "endbasic-std", + "flate2", + "futures-lite", + "once_cell", + "sdl2", + "tempfile", +] + +[[package]] +name = "endbasic-st7735s" +version = "0.12.0" +dependencies = [ + "async-channel", + "async-trait", + "endbasic-core", + "endbasic-std", + "tempfile", + "tokio", +] + +[[package]] +name = "endbasic-std" +version = "0.12.0" +dependencies = [ + "async-channel", + "async-trait", + "endbasic-core", + "filetime", + "flate2", + "futures-lite", + "getrandom 0.2.17", + "radix_trie", + "rand", + "tempfile", + "thiserror", + "time", + "tokio", +] + +[[package]] +name = "endbasic-terminal" +version = "0.12.0" +dependencies = [ + "async-channel", + "async-trait", + "crossterm", + "endbasic-core", + "endbasic-std", + "tokio", +] + +[[package]] +name = "endbasic-web" +version = "0.12.0" +dependencies = [ + "async-channel", + "async-trait", + "console_error_panic_hook", + "endbasic-client", + "endbasic-core", + "endbasic-repl", + "endbasic-std", + "js-sys", + "serde", + "serde_json", + "time", + "url", + "vergen", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getoptsargs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9d7fcbabff1eb3a1799271a46ef5902ba2c342f48ed4cae981ceddc6a15f95" +dependencies = [ + "anyhow", + "getopts", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rppal" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc171bbe325b04172e18d917c58c2cf1fb5adfd9ffabb1d6b3d62ba4c1c1331" +dependencies = [ + "libc", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdl2" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" +dependencies = [ + "anyhow", + "cfg-if", + "rustversion", + "time", +] + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29826f9d9ecaa314c480d376b276d1c790e6cb6a4681fab8532da69cbabf977d" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c610311887f9e6599a546d278d12d69dfd3a3e92639b2129e4b11ad6cf1961d6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60238e5b4b1b295701d6f9a66d2a126fe19990348f5fb9dae3b623a370119d94" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/NEWS.md b/NEWS.md index 8661578e..6988b4aa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,7 +14,8 @@ for the time being.** STILL UNDER DEVELOPMENT; NOT RELEASED YET. -* No changes recorded. +* Issue #278: Added a `Cargo.lock` file and pinned the Rust version to allow + for reproducible builds. ## Changes in version 0.12.0 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..4933b3ba --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.95" From fbfff5205caa413fe097f3fefb519c01c5ad0301 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 3 May 2026 16:46:13 +0200 Subject: [PATCH 106/110] Use sccache in GitHub actions --- .github/workflows/build.yml | 16 ++++++++++++++++ .github/workflows/deploy-release.yml | 4 ++++ .github/workflows/deploy-staging.yml | 4 ++++ .github/workflows/release.yml | 8 ++++++++ .github/workflows/test.yml | 17 +++++++++++++++++ 5 files changed, 49 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd0df311..414593b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,12 +20,16 @@ on: push jobs: linux-armv7-rpi: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable target: armv7-unknown-linux-gnueabihf - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: sudo apt update - run: sudo apt install gcc-arm-linux-gnueabihf - run: ./.github/workflows/release.sh linux-armv7-rpi @@ -37,8 +41,12 @@ jobs: linux-x86_64-sdl: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/release.sh linux-x86_64-sdl @@ -50,8 +58,12 @@ jobs: macos-arm64-sdl: runs-on: macos-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: ./.github/workflows/release.sh macos-arm64-sdl - uses: actions/upload-artifact@v7 with: @@ -61,8 +73,12 @@ jobs: windows-x86_64-sdl: runs-on: windows-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: choco install --allow-empty-checksums unzip zip - run: ./.github/workflows/setup-sdl.ps1 - run: ./.github/workflows/release.sh windows-x86_64-sdl diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 505a0797..9dcdd7e7 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -23,8 +23,12 @@ on: jobs: publish: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - uses: cargo-bins/cargo-binstall@main with: version: "1.18.1" diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 257406b2..e805cf3e 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -28,8 +28,12 @@ on: jobs: publish: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - uses: cargo-bins/cargo-binstall@main with: version: "1.18.1" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c1acaff..d02b285b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,16 +24,24 @@ on: jobs: cargo-package: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/package.sh cargo-install: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: ./.github/workflows/install.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9be082ef..de0b76f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,12 @@ on: push jobs: lint: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: rustup component add clippy rustfmt @@ -30,6 +34,8 @@ jobs: linux-test: runs-on: ubuntu-latest env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" SERVICE_URL: ${{ secrets.SERVICE_URL }} TEST_ACCOUNT_1_USERNAME: ${{ secrets.TEST_ACCOUNT_1_USERNAME }} TEST_ACCOUNT_1_PASSWORD: ${{ secrets.TEST_ACCOUNT_1_PASSWORD }} @@ -37,6 +43,7 @@ jobs: TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: sudo apt update - run: sudo apt install libsdl2-dev libsdl2-ttf-dev - run: cargo test --package='*' --features=sdl @@ -45,6 +52,8 @@ jobs: macos-test: runs-on: macos-latest env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" SERVICE_URL: ${{ secrets.SERVICE_URL }} TEST_ACCOUNT_1_USERNAME: ${{ secrets.TEST_ACCOUNT_1_USERNAME }} TEST_ACCOUNT_1_PASSWORD: ${{ secrets.TEST_ACCOUNT_1_PASSWORD }} @@ -52,6 +61,7 @@ jobs: TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: brew install sdl2 sdl2_ttf - run: cargo test --package=endbasic-client -- --include-ignored - run: cargo test --package=endbasic-core -- --include-ignored @@ -69,6 +79,8 @@ jobs: windows-test: runs-on: windows-latest env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" SERVICE_URL: ${{ secrets.SERVICE_URL }} TEST_ACCOUNT_1_USERNAME: ${{ secrets.TEST_ACCOUNT_1_USERNAME }} TEST_ACCOUNT_1_PASSWORD: ${{ secrets.TEST_ACCOUNT_1_PASSWORD }} @@ -76,6 +88,7 @@ jobs: TEST_ACCOUNT_2_PASSWORD: ${{ secrets.TEST_ACCOUNT_2_PASSWORD }} steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: choco install --allow-empty-checksums unzip - run: ./.github/workflows/setup-sdl.ps1 - run: cargo test --package=endbasic-client -- --include-ignored @@ -91,6 +104,10 @@ jobs: linux-test-no-features: runs-on: ubuntu-latest + env: + RUSTC_WRAPPER: "sccache" + SCCACHE_GHA_ENABLED: "true" steps: - uses: actions/checkout@v6 + - uses: mozilla-actions/sccache-action@v0.0.10 - run: cd std && cargo build --no-default-features From 30e770ab7501054ebba5009b35f630b2654b14f1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 3 May 2026 17:03:49 +0200 Subject: [PATCH 107/110] Be stricter in create-release During the last release process, I noticed that the draft release was created with the correct notes but that no artifacts were attached to it. I still don't know why that happened, but let's start by making the upload process more robust by verifying that the intermediate zips are properly downloaded and passed to the zip upload. --- .github/workflows/build.yml | 8 +++- .github/workflows/release-assets.sh | 66 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100755 .github/workflows/release-assets.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 414593b6..301f9a9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,6 +100,12 @@ jobs: uses: actions/download-artifact@v8 with: path: artifacts + - name: Collect release assets + id: assets + run: > + ./.github/workflows/release-assets.sh + linux-armv7-rpi linux-x86_64-sdl macos-arm64-sdl windows-x86_64-sdl + >>"$GITHUB_OUTPUT" - name: Create release draft id: create_release uses: ncipollo/release-action@v1 @@ -119,6 +125,6 @@ jobs: replacesArtifacts: false draft: true prerelease: false - artifacts: "artifacts/*/*.zip" + artifacts: ${{ steps.assets.outputs.artifacts }} artifactContentType: application/zip token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-assets.sh b/.github/workflows/release-assets.sh new file mode 100755 index 00000000..4aafc62d --- /dev/null +++ b/.github/workflows/release-assets.sh @@ -0,0 +1,66 @@ +#! /bin/sh +# 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. + +set -eu + +readonly PROGNAME="${0##*/}" + +err() { + echo "${PROGNAME}: ${*}" 1>&2 + exit 1 +} + +find_asset() { + local directory="${1}"; shift + + [ -d "${directory}" ] || err "Missing artifact directory: ${directory}" + + local count=0 + local asset= + for candidate in "${directory}"/*.zip; do + [ -f "${candidate}" ] || continue + count=$((count + 1)) + asset="${candidate}" + done + + [ "${count}" -eq 1 ] || err "Expected exactly one ZIP in ${directory}" + + echo "${asset}" +} + +main() { + local errors=0 + local assets= + for name in "${@}"; do + local directory="artifacts/endbasic-${name}" + local asset + if asset="$(find_asset "${directory}")"; then + if [ -z "${assets}" ]; then + assets="${asset}" + else + assets="${assets},${asset}" + fi + else + errors=$((errors + 1)) + fi + done + + [ "${errors}" -eq 0 ] || err "Found ${errors} release asset issue(s)" + + echo "artifacts=${assets}" +} + +main "${@}" From f139a90279b484aecb4a58369009f1f5c86a6387 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 3 May 2026 19:35:27 +0200 Subject: [PATCH 108/110] Merge release-actions into one There is no need for two separate steps as far as I can tell, so do both release creation and artifact uploads at once. --- .github/workflows/build.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 301f9a9e..ec084dfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,22 +107,12 @@ jobs: linux-armv7-rpi linux-x86_64-sdl macos-arm64-sdl windows-x86_64-sdl >>"$GITHUB_OUTPUT" - name: Create release draft - id: create_release - uses: ncipollo/release-action@v1 - with: - name: Release ${{ github.ref }} - bodyFile: ./notes.md - draft: true - prerelease: false - token: ${{ secrets.GITHUB_TOKEN }} - - name: Upload zip artifacts - id: upload_zips uses: ncipollo/release-action@v1 with: allowUpdates: true - omitBody: true - omitName: true replacesArtifacts: false + name: Release ${{ github.ref }} + bodyFile: ./notes.md draft: true prerelease: false artifacts: ${{ steps.assets.outputs.artifacts }} From 784febcc71e2d8af0ee3ede7c0da555750e0290f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 3 May 2026 19:54:57 +0200 Subject: [PATCH 109/110] Improve the RELEASE.md instructions Apply some changes based on my experience re-running this script after a long time. --- RELEASE.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 88c19ce2..22b5ebba 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,15 +3,21 @@ 1. Create a local branch called `release`. * **The name must be `release` to trigger required pre-release checks.** - These checks ensure that: `cargo publish` (detailed below) will succeed, - as it is an irreversible operation; that `cargo install` will work - for various feature combinations; and eagerly deploy the release to - staging. + These checks ensure that: + + 1. `cargo publish` (detailed below) will succeed, as it is an + irreversible operation. + 1. `cargo install` will work for various feature combinations. + 1. Eagerly deploy the release to staging. + * **Make sure the branch is synced to `origin/master`!** - * `git fetch` - * `git checkout -b release origin/master`. -1. Create a PR to update: + * `git fetch` + * `git checkout -b release origin/master`. + +1. Create a PR to update the version number everywhere necessary. You can + use the `.github/workflows/update-version.sh` script to do this, but then + make sure that following files were modified as described: * `NEWS.md`: Set next version number, add date, and clean up notes. * `README.md`: Update latest version number and release date. @@ -19,10 +25,6 @@ * `web/package*.json`: Update version number. * `.github/workflows/deploy-release.yml`: Update tag number. - The `.github/workflows/update-version.sh` script should prepare a PR with - all of the required changes. Don't forget to clean up the release notes - though. - 1. Once all tests pass, merge the PR. 1. Tag the resulting merged commit as `endbasic-X.Y.Z` and push the tag. This @@ -33,12 +35,8 @@ 1. Push the new crates out. This is the last step because it's not reversible: - * `( cd core && cargo publish )` - * `( cd std && cargo publish )` - * `( cd repl && cargo publish )` - * `( cd client && cargo publish )` - * `( cd terminal && cargo publish )` - * `( cd sdl && cargo publish )` - * `( cd st7735s && cargo publish )` - * `( cd rpi && cargo publish )` - * `( cd cli && cargo publish )` + ``` + for c in core std repl client terminal sdl st7735s rpi cli; do + ( cd $c && cargo publish ) || break + done + ``` From e0621d53a1282299b5378f7d74ce3d493e52cd0c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 3 May 2026 20:01:23 +0200 Subject: [PATCH 110/110] Release EndBASIC 0.12.1 --- .github/workflows/deploy-release.yml | 2 +- Cargo.lock | 20 ++++++++++---------- NEWS.md | 2 +- README.md | 4 ++-- RELEASE.md | 2 ++ cli/Cargo.toml | 18 +++++++++--------- client/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- repl/Cargo.toml | 6 +++--- rpi/Cargo.toml | 4 ++-- sdl/Cargo.toml | 6 +++--- st7735s/Cargo.toml | 6 +++--- std/Cargo.toml | 4 ++-- terminal/Cargo.toml | 6 +++--- web/Cargo.toml | 10 +++++----- web/package-lock.json | 4 ++-- web/package.json | 2 +- 17 files changed, 53 insertions(+), 51 deletions(-) diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 9dcdd7e7..721768d3 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -18,7 +18,7 @@ name: Deploy to repl.endbasic.dev on: push: tags: - - endbasic-0.12.0 + - endbasic-0.12.1 jobs: publish: diff --git a/Cargo.lock b/Cargo.lock index ddf577eb..7788d8ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,7 +242,7 @@ dependencies = [ [[package]] name = "endbasic" -version = "0.12.0" +version = "0.12.1" dependencies = [ "anyhow", "async-channel", @@ -264,7 +264,7 @@ dependencies = [ [[package]] name = "endbasic-client" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-trait", "base64", @@ -283,7 +283,7 @@ dependencies = [ [[package]] name = "endbasic-core" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-channel", "async-trait", @@ -294,7 +294,7 @@ dependencies = [ [[package]] name = "endbasic-repl" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-trait", "endbasic-core", @@ -305,7 +305,7 @@ dependencies = [ [[package]] name = "endbasic-rpi" -version = "0.12.0" +version = "0.12.1" dependencies = [ "endbasic-std", "rppal", @@ -313,7 +313,7 @@ dependencies = [ [[package]] name = "endbasic-sdl" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-channel", "async-trait", @@ -328,7 +328,7 @@ dependencies = [ [[package]] name = "endbasic-st7735s" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-channel", "async-trait", @@ -340,7 +340,7 @@ dependencies = [ [[package]] name = "endbasic-std" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-channel", "async-trait", @@ -359,7 +359,7 @@ dependencies = [ [[package]] name = "endbasic-terminal" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-channel", "async-trait", @@ -371,7 +371,7 @@ dependencies = [ [[package]] name = "endbasic-web" -version = "0.12.0" +version = "0.12.1" dependencies = [ "async-channel", "async-trait", diff --git a/NEWS.md b/NEWS.md index 6988b4aa..4b10e754 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,7 +12,7 @@ for the time being.** ## Changes in version 0.12.1 -STILL UNDER DEVELOPMENT; NOT RELEASED YET. +**Released on 2026-05-03.** * Issue #278: Added a `Cargo.lock` file and pinned the Rust version to allow for reproducible builds. diff --git a/README.md b/README.md index 29bfa053..f068bd04 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ operating systems and platforms, including macOS, Windows, and Linux. EndBASIC is free software under the [Apache 2.0 License](LICENSE). -**The latest version of EndBASIC is 0.12.0 and was released on 2026-05-02.** +**The latest version of EndBASIC is 0.12.1 and was released on 2026-05-03.** ## Quick start on the web @@ -46,7 +46,7 @@ for the cloud service and upload your programs to share them with the world. ## Quick start on your machine Visit the -[release page](https://github.com/endbasic/endbasic/releases/tag/endbasic-0.12.0) +[release page](https://github.com/endbasic/endbasic/releases/tag/endbasic-0.12.1) to download prebuilt binaries. Once downloaded, unpack the archive and run the `endbasic` binary to get started. diff --git a/RELEASE.md b/RELEASE.md index 22b5ebba..55f1bfcb 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -22,6 +22,8 @@ * `NEWS.md`: Set next version number, add date, and clean up notes. * `README.md`: Update latest version number and release date. * `*/Cargo.toml`: Update version number and `endbasic-*` dependencies. + * `Cargo.lock`: Reflects the new version number. Do a `cargo build` to + refresh it after running the script above. * `web/package*.json`: Update version number. * `.github/workflows/deploy-release.yml`: Update tag number. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 9b73119f..f0b4eee2 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -28,38 +28,38 @@ getoptsargs = "0.1" thiserror = "1.0" [dependencies.endbasic-client] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../client" [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-repl] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../repl" [dependencies.endbasic-rpi] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../rpi" optional = true [dependencies.endbasic-sdl] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../sdl" optional = true [dependencies.endbasic-st7735s] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../st7735s" optional = true [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" [dependencies.endbasic-terminal] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../terminal" optional = true diff --git a/client/Cargo.toml b/client/Cargo.toml index 4a31e920..a8393507 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-client" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -24,11 +24,11 @@ time = { version = "0.3", features = ["std"] } url = "2.2" [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" [dependencies.reqwest] diff --git a/core/Cargo.toml b/core/Cargo.toml index c8c37a8e..e1e7c68e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-core" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] diff --git a/repl/Cargo.toml b/repl/Cargo.toml index 0328996c..42fd92a5 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-repl" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -19,11 +19,11 @@ async-trait = "0.1" time = { version = "0.3", features = ["std"] } [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" [dev-dependencies] diff --git a/rpi/Cargo.toml b/rpi/Cargo.toml index dc854ee2..94de7b34 100644 --- a/rpi/Cargo.toml +++ b/rpi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-rpi" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -18,5 +18,5 @@ workspace = true rppal = "0.17" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" diff --git a/sdl/Cargo.toml b/sdl/Cargo.toml index e92670ac..9d8e4418 100644 --- a/sdl/Cargo.toml +++ b/sdl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-sdl" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -21,11 +21,11 @@ once_cell = "1.8" tempfile = "3" [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" [dependencies.sdl2] diff --git a/st7735s/Cargo.toml b/st7735s/Cargo.toml index b6639a46..c7b2d699 100644 --- a/st7735s/Cargo.toml +++ b/st7735s/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-st7735s" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -20,11 +20,11 @@ async-trait = "0.1" tokio = { version = "1", features = ["full"] } [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" [dev-dependencies] diff --git a/std/Cargo.toml b/std/Cargo.toml index 2b64e191..f554fe2a 100644 --- a/std/Cargo.toml +++ b/std/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-std" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -23,7 +23,7 @@ thiserror = "1.0" time = { version = "0.3", features = ["formatting", "local-offset", "std"] } [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" # We don't directly use getrandom but rand does, and we have to customize how diff --git a/terminal/Cargo.toml b/terminal/Cargo.toml index b569bdbf..a79b31cd 100644 --- a/terminal/Cargo.toml +++ b/terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-terminal" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -21,10 +21,10 @@ crossterm = "0.27" tokio = { version = "1", features = ["rt"] } [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" diff --git a/web/Cargo.toml b/web/Cargo.toml index 91a8d453..35d10f58 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "endbasic-web" -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION license = "Apache-2.0" authors = ["Julio Merino "] categories = ["development-tools", "parser-implementations"] @@ -35,19 +35,19 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" [dependencies.endbasic-client] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../client" [dependencies.endbasic-core] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../core" [dependencies.endbasic-repl] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../repl" [dependencies.endbasic-std] -version = "0.12.0" # ENDBASIC-VERSION +version = "0.12.1" # ENDBASIC-VERSION path = "../std" [dependencies.web-sys] diff --git a/web/package-lock.json b/web/package-lock.json index a0af91bc..13c024b5 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "endbasic-www", - "version": "0.12.0", + "version": "0.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "endbasic-www", - "version": "0.12.0", + "version": "0.12.1", "license": "Apache-2.0", "dependencies": { "jquery": "3.7.1" diff --git a/web/package.json b/web/package.json index 797b8d7d..f907619c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "endbasic-www", - "version": "0.12.0", + "version": "0.12.1", "description": "The EndBASIC programming language - web interface", "private": true, "scripts": {