geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::{
    GleamTimeHostProfile, GleamTimeProfile, GleamTimeRunState, TimeProvider, TimeSource,
    host_providers,
};
use crate::gleam_stdlib::{
    GleamStdlibHostProfile, GleamStdlibRunState, GleamStdlibStores, IoOutput, IoSink,
};
use crate::{
    ExecutionError, HostFailure, HostModule, HostProfile, HostProvider, HostProviderSet,
    HostedExecution, ModuleSource, PackageSource, compile_typed_host_program, plan_host_program,
};
use ecow::EcoString;
use std::collections::VecDeque;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

struct CustomProfile;

#[derive(Default)]
struct CustomStores {
    stdlib: GleamStdlibStores,
}

struct CustomRunState {
    stdlib: GleamStdlibRunState,
    source: ScriptedSource,
    io: RecordingSink,
}

#[derive(Default)]
struct RecordingSink {
    outputs: Vec<IoOutput>,
}

#[derive(Default)]
struct ScriptedSource {
    times: VecDeque<Result<SystemTime, HostFailure>>,
    offsets: VecDeque<Result<i32, HostFailure>>,
}

impl IoSink for RecordingSink {
    fn emit(&mut self, output: IoOutput) {
        self.outputs.push(output);
    }
}

impl TimeSource for ScriptedSource {
    fn system_time(&mut self) -> Result<SystemTime, HostFailure> {
        self.times
            .pop_front()
            .unwrap_or_else(|| Err(HostFailure::new("scripted system time exhausted")))
    }

    fn local_offset_seconds(&mut self) -> Result<i32, HostFailure> {
        self.offsets
            .pop_front()
            .unwrap_or_else(|| Err(HostFailure::new("scripted local offset exhausted")))
    }
}

impl HostProfile for CustomProfile {
    type RunState = CustomRunState;
    type ExternalStores = CustomStores;
}

impl GleamStdlibHostProfile for CustomProfile {
    type Io = RecordingSink;

    fn gleam_stdlib_stores(stores: &Self::ExternalStores) -> &GleamStdlibStores {
        &stores.stdlib
    }

    fn gleam_stdlib_run_state(state: &mut Self::RunState) -> &mut GleamStdlibRunState {
        &mut state.stdlib
    }

    fn gleam_stdlib_io(state: &mut Self::RunState) -> &mut Self::Io {
        &mut state.io
    }
}

impl GleamTimeHostProfile for CustomProfile {
    type Source = ScriptedSource;

    fn gleam_time_source(state: &mut Self::RunState) -> &mut Self::Source {
        &mut state.source
    }
}

const CALENDAR_SOURCE: &str = r#"
@external(erlang, "gleam_time_ffi", "local_time_offset_seconds")
@external(javascript, "../../gleam_time_ffi.mjs", "local_time_offset_seconds")
fn local_time_offset_seconds() -> Int

pub fn current_offset() -> Int {
  local_time_offset_seconds()
}
"#;

const TIMESTAMP_SOURCE: &str = r#"
@external(erlang, "gleam_time_ffi", "system_time")
@external(javascript, "../../gleam_time_ffi.mjs", "system_time")
fn get_system_time() -> #(Int, Int)

pub fn current_parts() -> #(Int, Int) {
  get_system_time()
}
"#;

const MAIN_SOURCE: &str = r#"
import gleam/time/calendar
import gleam/time/timestamp

pub fn main() {
  #(
    timestamp.current_parts(),
    calendar.current_offset(),
    timestamp.current_parts(),
    calendar.current_offset(),
  )
}
"#;

#[test]
fn default_and_custom_profiles_project_owned_state_stores_source_and_io() {
    let default_stores = GleamStdlibStores::default();
    let custom_stores = CustomStores::default();
    let mut default_state = GleamTimeRunState::new(
        GleamStdlibRunState::from_seed([1; 32]),
        ScriptedSource::default(),
    );
    let mut custom_state = CustomRunState {
        stdlib: GleamStdlibRunState::from_seed([2; 32]),
        source: ScriptedSource::default(),
        io: RecordingSink::default(),
    };

    assert!(std::ptr::eq(
        GleamTimeProfile::<ScriptedSource>::gleam_stdlib_stores(&default_stores),
        &default_stores,
    ));
    assert!(std::ptr::eq(
        CustomProfile::gleam_stdlib_stores(&custom_stores),
        &custom_stores.stdlib,
    ));
    let default_stdlib = default_state.stdlib() as *const GleamStdlibRunState;
    assert!(std::ptr::eq(
        GleamTimeProfile::<ScriptedSource>::gleam_stdlib_run_state(&mut default_state),
        default_stdlib,
    ));
    let custom_stdlib = &custom_state.stdlib as *const GleamStdlibRunState;
    assert!(std::ptr::eq(
        CustomProfile::gleam_stdlib_run_state(&mut custom_state),
        custom_stdlib,
    ));

    let default_source = default_state.source() as *const ScriptedSource;
    assert!(std::ptr::eq(
        GleamTimeProfile::<ScriptedSource>::gleam_time_source(&mut default_state),
        default_source,
    ));
    let custom_source = &custom_state.source as *const ScriptedSource;
    assert!(std::ptr::eq(
        CustomProfile::gleam_time_source(&mut custom_state),
        custom_source,
    ));
    let provider_source = default_state.source() as *const ScriptedSource;
    assert!(std::ptr::eq(
        <TimeProvider<GleamTimeProfile<ScriptedSource>> as HostProvider<
            GleamTimeProfile<ScriptedSource>,
        >>::project(&mut default_state),
        provider_source,
    ));

    assert!(default_state.stdlib_mut().take_io_outputs().is_empty());
    assert!(GleamTimeProfile::<ScriptedSource>::gleam_stdlib_io(&mut default_state).is_empty(),);
    assert!(
        CustomProfile::gleam_stdlib_io(&mut custom_state)
            .outputs
            .is_empty()
    );
    default_state.source_mut().offsets.push_back(Ok(3600));

    assert!(default_state.stdlib().io_outputs().is_empty());
    assert_eq!(default_state.source().offsets.len(), 1);
    assert!(custom_state.io.outputs.is_empty());
}

#[test]
fn registers_calendar_then_timestamp_without_external_types() {
    let providers =
        host_providers::<GleamTimeProfile>().expect("official Time providers should register");

    assert_eq!(
        providers
            .iter()
            .map(|provider| (provider.package().as_str(), provider.module().as_str()))
            .collect::<Vec<_>>(),
        [
            ("gleam_time", "gleam/time/calendar"),
            ("gleam_time", "gleam/time/timestamp"),
        ],
    );
    assert!(
        providers
            .iter()
            .all(|provider| provider.external_types().count() == 0),
    );
}

#[test]
fn executes_non_monotonic_time_and_changing_offsets_in_source_order() {
    let execution = execution::<ScriptedSource>(MAIN_SOURCE, "main");
    let mut first_state = scripted_state();
    let mut independent_state = scripted_state();

    let first = execution
        .run_main(&mut first_state, &mut Vec::new())
        .expect("scripted Time source should run");
    let repeated = execution
        .run_main(&mut first_state, &mut Vec::new())
        .expect("scripted Time source should run repeatedly");
    let independent = execution
        .run_main(&mut independent_state, &mut Vec::new())
        .expect("independent scripted Time source should run");

    assert_eq!(
        first.inspect().to_string(),
        "#(#(5, 0), 3600, #(-1, 999999999), -18000)",
    );
    assert_eq!(
        repeated.inspect().to_string(),
        "#(#(8, 7), 7200, #(-3, 999999997), 0)",
    );
    assert_eq!(independent, first);
    assert!(first_state.source().times.is_empty());
    assert!(first_state.source().offsets.is_empty());
}

#[test]
fn preserves_calendar_and_timestamp_source_failures() {
    for (module, source, failure, expected_function, expected_failure, expected_line) in [
        (
            "gleam/time/calendar",
            format!("{CALENDAR_SOURCE}\npub fn main() {{\n  current_offset()\n}}\n",),
            ScriptedSource {
                times: VecDeque::new(),
                offsets: [Err(HostFailure::new("offset unavailable"))].into(),
            },
            "local_time_offset_seconds",
            "offset unavailable",
            7,
        ),
        (
            "gleam/time/timestamp",
            format!("{TIMESTAMP_SOURCE}\npub fn main() {{\n  current_parts()\n}}\n",),
            ScriptedSource {
                times: [Err(HostFailure::new("clock unavailable"))].into(),
                offsets: VecDeque::new(),
            },
            "get_system_time",
            "clock unavailable",
            7,
        ),
    ] {
        let execution = execution::<ScriptedSource>(&source, module);
        let mut state = GleamTimeRunState::new(GleamStdlibRunState::from_seed([4; 32]), failure);
        let error = execution
            .run_main(&mut state, &mut Vec::new())
            .expect_err("scripted Time failure should remain an execution error");
        let ExecutionError::Host(error) = error else {
            panic!("scripted Time failure should remain a host error");
        };

        assert_eq!(error.package(), "gleam_time");
        assert_eq!(error.module(), module);
        assert_eq!(error.function(), expected_function);
        assert_eq!(error.failure().message(), expected_failure);
        assert_eq!(
            error
                .location()
                .path()
                .expect("synthetic Time failure should retain its source path")
                .as_str(),
            format!("src/{module}.gleam"),
        );
        assert_eq!(error.location().line(), Some(expected_line));
    }
}

fn scripted_state() -> GleamTimeRunState<ScriptedSource> {
    GleamTimeRunState::new(
        GleamStdlibRunState::from_seed([3; 32]),
        ScriptedSource {
            times: [
                Ok(UNIX_EPOCH + Duration::from_secs(5)),
                Ok(UNIX_EPOCH - Duration::from_nanos(1)),
                Ok(UNIX_EPOCH + Duration::new(8, 7)),
                Ok(UNIX_EPOCH - Duration::new(2, 3)),
            ]
            .into(),
            offsets: [Ok(3600), Ok(-18_000), Ok(7200), Ok(0)].into(),
        },
    )
}

fn execution<Source>(source: &str, root_module: &str) -> HostedExecution<GleamTimeProfile<Source>>
where
    Source: TimeSource,
{
    let modules = if root_module == "main" {
        vec![
            ModuleSource::new(
                "gleam/time/calendar",
                "src/gleam/time/calendar.gleam",
                CALENDAR_SOURCE,
            ),
            ModuleSource::new(
                "gleam/time/timestamp",
                "src/gleam/time/timestamp.gleam",
                TIMESTAMP_SOURCE,
            ),
            ModuleSource::new("main", "src/main.gleam", source),
        ]
    } else {
        vec![
            ModuleSource::new(
                "gleam/time/calendar",
                "src/gleam/time/calendar.gleam",
                if root_module == "gleam/time/calendar" {
                    source
                } else {
                    CALENDAR_SOURCE
                },
            ),
            ModuleSource::new(
                "gleam/time/timestamp",
                "src/gleam/time/timestamp.gleam",
                if root_module == "gleam/time/timestamp" {
                    source
                } else {
                    TIMESTAMP_SOURCE
                },
            ),
        ]
    };
    let providers = host_providers::<GleamTimeProfile<Source>>()
        .expect("synthetic Time providers should register");
    let hosts = HostProviderSet::with_providers(
        Vec::<HostModule<GleamTimeProfile<Source>>>::new(),
        providers,
    )
    .expect("synthetic Time provider modules should be unique");
    let typed = compile_typed_host_program(
        "gleam_time",
        root_module,
        [PackageSource::new(
            "gleam_time",
            Vec::<EcoString>::new(),
            modules,
        )],
        hosts,
    )
    .expect("synthetic Time source should compile");
    let plan = plan_host_program(typed).expect("synthetic Time source should plan");
    HostedExecution::try_from_module_plan(plan).expect("synthetic Time execution should seal")
}