endbasic-std 0.14.0

The EndBASIC programming language - standard library
Documentation
// EndBASIC
// Copyright 2026 Julio Merino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

//! Time manipulation primitives.

use async_trait::async_trait;
use endbasic_core::{
    ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
    ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
};
use std::borrow::Cow;
use std::rc::Rc;
use std::thread;
use std::time::{Duration, Instant};

use crate::MachineBuilder;

/// Category description for all symbols provided by this module.
pub(crate) const CATEGORY: &str = "Date and time";

/// Interface to date and time functionality provided by the host.
#[async_trait(?Send)]
pub trait DateTime {
    /// Returns the current monotonic timestamp.
    fn monotonic(&self) -> Duration;

    /// Suspends execution for the given duration.
    async fn sleep(&self, d: Duration) -> Result<(), String>;
}

/// Default host-backed implementation of the `DateTime` trait.
pub struct SystemDateTime {
    epoch: Instant,
}

impl Default for SystemDateTime {
    fn default() -> Self {
        Self { epoch: Instant::now() }
    }
}

#[async_trait(?Send)]
impl DateTime for SystemDateTime {
    fn monotonic(&self) -> Duration {
        self.epoch.elapsed()
    }

    async fn sleep(&self, d: Duration) -> Result<(), String> {
        thread::sleep(d);
        Ok(())
    }
}

/// The `MONOTONIC` function.
pub struct MonotonicFunction {
    metadata: Rc<CallableMetadata>,
    datetime: Rc<dyn DateTime>,
}

impl MonotonicFunction {
    /// Creates a new instance of the function.
    pub fn new(datetime: Rc<dyn DateTime>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("MONOTONIC")
                .with_return_type(ExprType::Double)
                .with_syntax(&[(&[], None)])
                .with_category(CATEGORY)
                .with_description(
                    "Returns a monotonic timestamp.
The returned value is the number of seconds since an unspecified reference point.  The timestamp \
is only useful to compute elapsed time by subtraction and is not related to the current date or \
wall-clock time.",
                )
                .build(),
            datetime,
        })
    }
}

#[async_trait(?Send)]
impl Callable for MonotonicFunction {
    fn metadata(&self) -> Rc<CallableMetadata> {
        self.metadata.clone()
    }

    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
        debug_assert_eq!(0, scope.nargs());
        scope.return_double(self.datetime.monotonic().as_secs_f64())
    }
}

/// The `SLEEP` command.
pub struct SleepCommand {
    metadata: Rc<CallableMetadata>,
    datetime: Rc<dyn DateTime>,
}

impl SleepCommand {
    /// Creates a new instance of the command.
    pub fn new(datetime: Rc<dyn DateTime>) -> Rc<Self> {
        Rc::from(Self {
            metadata: CallableMetadataBuilder::new("SLEEP")
                .with_async(true)
                .with_syntax(&[(
                    &[SingularArgSyntax::RequiredValue(
                        RequiredValueSyntax {
                            name: Cow::Borrowed("seconds"),
                            vtype: ExprType::Double,
                        },
                        ArgSepSyntax::End,
                    )],
                    None,
                )])
                .with_category(CATEGORY)
                .with_description(
                    "Suspends program execution.
Pauses program execution for the given number of seconds, which can be specified either as an \
integer or as a floating point number for finer precision.",
                )
                .build(),
            datetime,
        })
    }
}

#[async_trait(?Send)]
impl Callable for SleepCommand {
    fn metadata(&self) -> Rc<CallableMetadata> {
        self.metadata.clone()
    }

    async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
        debug_assert_eq!(1, scope.nargs());
        let n = scope.get_double(0);

        if n < 0.0 {
            return Err(CallError::Syntax(
                scope.get_pos(0),
                "Sleep time must be positive".to_owned(),
            ));
        }

        self.datetime
            .sleep(Duration::from_secs_f64(n))
            .await
            .map_err(|e| CallError::Syntax(scope.get_pos(0), e))
    }
}

/// Adds all symbols provided by this module to the given `machine`.
///
/// `datetime` provides access to date and time functionality.
pub fn add_all(machine: &mut MachineBuilder, datetime: Rc<dyn DateTime>) {
    machine.add_callable(MonotonicFunction::new(datetime.clone()));
    machine.add_callable(SleepCommand::new(datetime));
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::MachineBuilder;
    use crate::testutils::*;
    use futures_lite::future::block_on;
    use std::time::Instant;

    #[test]
    fn test_monotonic_ok() {
        let mut tester = Tester::default();
        tester.get_datetime().add_monotonic(Duration::from_millis(12_345));
        tester.run("result = MONOTONIC").expect_var("result", 12.345).check();
    }

    #[test]
    fn test_monotonic_errors() {
        check_expr_compilation_error("1:10: MONOTONIC expected no arguments", "MONOTONIC()");
    }

    #[test]
    fn test_sleep_ok_int() {
        let mut tester = Tester::default();
        tester.run("SLEEP 123").check();
        assert_eq!(vec![Duration::from_secs(123)], tester.get_datetime().sleeps());
    }

    #[test]
    fn test_sleep_ok_float() {
        let mut tester = Tester::default();
        tester.run("SLEEP 123.1").check();
        let sleeps = tester.get_datetime().sleeps();
        assert_eq!(1, sleeps.len());
        let ms = sleeps[0].as_millis();
        assert!(ms > 123095 && ms < 123105, "Bad {}", ms);
    }

    #[test]
    fn test_sleep_real() {
        let before = Instant::now();
        let mut machine = MachineBuilder::default().build();
        machine.compile(&mut "SLEEP 0.010".as_bytes()).unwrap();
        match block_on(machine.exec()) {
            Ok(None) => (),
            r => panic!("Expected Ok(None) but got {:?}", r),
        }
        assert!(before.elapsed() >= Duration::from_millis(10));
    }

    #[test]
    fn test_sleep_errors() {
        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:7: Sleep time must be positive", "SLEEP -1");
        check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
    }
}