extern crate std;
use std::io::Write;
use std::process::Command;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::address::Address;
use crate::bytecode::GenericValue;
use crate::float::Float;
use crate::vm::{GenericVm, NativeCtx, VmError};
use crate::word::Word;
std::thread_local! {
static SCRIPT_ARGS: std::cell::RefCell<std::option::Option<std::vec::Vec<std::string::String>>> =
const { std::cell::RefCell::new(std::option::Option::None) };
}
pub fn set_script_args(args: std::vec::Vec<std::string::String>) {
SCRIPT_ARGS.with(|cell| *cell.borrow_mut() = std::option::Option::Some(args));
}
pub fn clear_script_args() {
SCRIPT_ARGS.with(|cell| *cell.borrow_mut() = std::option::Option::None);
}
fn script_arg_count() -> i64 {
SCRIPT_ARGS.with(|cell| match &*cell.borrow() {
std::option::Option::Some(v) => v.len() as i64,
std::option::Option::None => std::env::args().count() as i64,
})
}
fn script_arg_at(index: usize) -> std::option::Option<std::string::String> {
SCRIPT_ARGS.with(|cell| match &*cell.borrow() {
std::option::Option::Some(v) => v.get(index).cloned(),
std::option::Option::None => std::env::args().nth(index),
})
}
pub fn register<'a, 'arena, W: Word, A: Address, F: Float>(
vm: &mut GenericVm<'a, 'arena, W, A, F>,
) {
vm.register_native_with_ctx("shell::getenv", getenv_native::<W, F>);
vm.register_native_with_ctx("shell::has_env", has_env_native::<W, F>);
vm.register_native_with_ctx("shell::run", run_native::<W, F>);
vm.register_native_with_ctx("shell::run_full", run_full_native::<W, F>);
vm.register_native_with_ctx("shell::run_checked", run_checked_native::<W, F>);
vm.register_native("shell::exit", exit_native::<W, F>);
vm.register_native("shell::sleep_ms", sleep_ms_native::<W, F>);
vm.register_native("shell::now_unix_ms", now_unix_ms_native::<W, F>);
vm.register_native_with_ctx("shell::read_file", read_file_native::<W, F>);
vm.register_native_with_ctx("shell::write_file", write_file_native::<W, F>);
vm.register_native_with_ctx("shell::append_file", append_file_native::<W, F>);
vm.register_native_with_ctx("shell::file_exists", file_exists_native::<W, F>);
vm.register_native_with_ctx("shell::write_err", write_err_native::<W, F>);
vm.register_native_with_ctx("shell::writeln_err", writeln_err_native::<W, F>);
vm.register_native("shell::pid", pid_native::<W, F>);
vm.register_native("shell::hostname", hostname_native::<W, F>);
vm.register_native("shell::arg_count", arg_count_native::<W, F>);
vm.register_native("shell::arg", arg_native::<W, F>);
vm.register_native_with_ctx("shell::setenv", setenv_native::<W, F>);
vm.register_native("shell::pwd", pwd_native::<W, F>);
vm.register_native_with_ctx("shell::cd", cd_native::<W, F>);
vm.register_native_with_ctx("shell::run_timeout", run_timeout_native::<W, F>);
}
fn arg_str<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
idx: usize,
expect_label: &str,
) -> Result<std::string::String, VmError> {
match args[idx].as_str_with_arena(ctx.arena) {
Ok(std::option::Option::Some(s)) => Ok(std::string::String::from(s)),
Ok(std::option::Option::None) => Err(VmError::TypeError(std::format!(
"{}, got {}",
expect_label,
args[idx].type_name()
))),
Err(_) => Err(VmError::TypeError(std::format!(
"{}, got a stale dynamic string (arena reset since it was produced)",
expect_label
))),
}
}
fn has_env_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::has_env: expected exactly one argument",
)));
}
let name = arg_str(ctx, args, 0, "shell::has_env: expected Text")?;
let name: &str = &name;
Ok(GenericValue::Bool(std::env::var(name).is_ok()))
}
fn exit_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::exit: expected exactly one argument",
)));
}
let code = match args[0] {
GenericValue::Int(n) => W::to_i64(n),
ref v => {
return Err(VmError::TypeError(std::format!(
"shell::exit: expected Word, got {}",
v.type_name()
)));
}
};
std::process::exit(code as i32);
}
fn getenv_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::getenv: expected exactly one argument",
)));
}
let name = arg_str(ctx, args, 0, "shell::getenv: expected Text")?;
let name: &str = &name;
match std::env::var(name) {
Ok(value) => Ok(GenericValue::Enum(crate::bytecode::EnumBody::boxed(
std::string::String::from("Option"),
std::string::String::from("Some"),
std::vec![GenericValue::StaticStr(value)],
))),
Err(std::env::VarError::NotPresent) => Ok(GenericValue::None),
Err(std::env::VarError::NotUnicode(_)) => Err(VmError::NativeError(std::format!(
"shell::getenv: {} is not valid Unicode",
name
))),
}
}
fn run_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::run: expected exactly one argument",
)));
}
let cmd = arg_str(ctx, args, 0, "shell::run: expected Text")?;
let cmd: &str = &cmd;
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()
.map_err(|e| VmError::NativeError(std::format!("shell::run: failed to spawn sh: {}", e)))?;
let exit_code = output.status.code().unwrap_or(-1) as i64;
let stdout = std::string::String::from_utf8_lossy(&output.stdout).into_owned();
Ok(GenericValue::tuple(std::vec![
GenericValue::Int(W::from_i64_wrap(exit_code)),
GenericValue::StaticStr(stdout),
]))
}
fn run_full_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::run_full: expected exactly one argument",
)));
}
let cmd = arg_str(ctx, args, 0, "shell::run_full: expected Text")?;
let cmd: &str = &cmd;
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()
.map_err(|e| {
VmError::NativeError(std::format!("shell::run_full: failed to spawn sh: {}", e))
})?;
let exit_code = output.status.code().unwrap_or(-1) as i64;
let stdout = std::string::String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = std::string::String::from_utf8_lossy(&output.stderr).into_owned();
Ok(GenericValue::tuple(std::vec![
GenericValue::Int(W::from_i64_wrap(exit_code)),
GenericValue::StaticStr(stdout),
GenericValue::StaticStr(stderr),
]))
}
fn run_checked_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::run_checked: expected exactly one argument",
)));
}
let cmd = arg_str(ctx, args, 0, "shell::run_checked: expected Text")?;
let cmd: &str = &cmd;
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()
.map_err(|e| {
VmError::NativeError(std::format!(
"shell::run_checked: failed to spawn sh: {}",
e
))
})?;
if !output.status.success() {
let code = output.status.code().unwrap_or(-1);
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
return Err(VmError::NativeError(std::format!(
"shell::run_checked: command exited with code {}: {}",
code,
stderr
)));
}
let stdout = std::string::String::from_utf8_lossy(&output.stdout).into_owned();
Ok(GenericValue::StaticStr(stdout))
}
fn sleep_ms_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::sleep_ms: expected exactly one argument",
)));
}
let ms = match args[0] {
GenericValue::Int(n) => W::to_i64(n),
ref v => {
return Err(VmError::TypeError(std::format!(
"shell::sleep_ms: expected Word, got {}",
v.type_name()
)));
}
};
if ms > 0 {
std::thread::sleep(std::time::Duration::from_millis(ms as u64));
}
Ok(GenericValue::Unit)
}
fn now_unix_ms_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if !args.is_empty() {
return Err(VmError::NativeError(std::string::String::from(
"shell::now_unix_ms: expected zero arguments",
)));
}
let dur = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e| {
VmError::NativeError(std::format!(
"shell::now_unix_ms: system clock is before the Unix epoch: {}",
e
))
})?;
let ms = dur.as_millis();
let clamped = if ms > i64::MAX as u128 {
i64::MAX
} else {
ms as i64
};
Ok(GenericValue::Int(W::from_i64_wrap(clamped)))
}
fn read_file_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::read_file: expected exactly one argument",
)));
}
let path = arg_str(ctx, args, 0, "shell::read_file: expected Text")?;
let path: &str = &path;
let bytes = std::fs::read(path)
.map_err(|e| VmError::NativeError(std::format!("shell::read_file: {}: {}", path, e)))?;
let text = std::string::String::from_utf8(bytes).map_err(|e| {
VmError::NativeError(std::format!(
"shell::read_file: {}: invalid UTF-8: {}",
path,
e
))
})?;
Ok(GenericValue::StaticStr(text))
}
fn write_file_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 2 {
return Err(VmError::NativeError(std::string::String::from(
"shell::write_file: expected exactly two arguments",
)));
}
let path = arg_str(ctx, args, 0, "shell::write_file: expected Text for path")?;
let path: &str = &path;
let content = arg_str(ctx, args, 1, "shell::write_file: expected Text for content")?;
let content: &str = &content;
std::fs::write(path, content)
.map_err(|e| VmError::NativeError(std::format!("shell::write_file: {}: {}", path, e)))?;
Ok(GenericValue::Unit)
}
fn append_file_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 2 {
return Err(VmError::NativeError(std::string::String::from(
"shell::append_file: expected exactly two arguments",
)));
}
let path = arg_str(ctx, args, 0, "shell::append_file: expected Text for path")?;
let path: &str = &path;
let content = arg_str(
ctx,
args,
1,
"shell::append_file: expected Text for content",
)?;
let content: &str = &content;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| {
VmError::NativeError(std::format!("shell::append_file: open {}: {}", path, e))
})?;
file.write_all(content.as_bytes()).map_err(|e| {
VmError::NativeError(std::format!("shell::append_file: write {}: {}", path, e))
})?;
Ok(GenericValue::Unit)
}
fn file_exists_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::file_exists: expected exactly one argument",
)));
}
let path = arg_str(ctx, args, 0, "shell::file_exists: expected Text")?;
let path: &str = &path;
Ok(GenericValue::Bool(std::path::Path::new(path).exists()))
}
fn write_err_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::write_err: expected exactly one argument",
)));
}
let text = arg_str(ctx, args, 0, "shell::write_err: expected Text")?;
let text: &str = &text;
let stderr = std::io::stderr();
let mut handle = stderr.lock();
handle
.write_all(text.as_bytes())
.map_err(|e| VmError::NativeError(std::format!("shell::write_err: {}", e)))?;
Ok(GenericValue::Unit)
}
fn writeln_err_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::writeln_err: expected exactly one argument",
)));
}
let text = arg_str(ctx, args, 0, "shell::writeln_err: expected Text")?;
let text: &str = &text;
let stderr = std::io::stderr();
let mut handle = stderr.lock();
writeln!(handle, "{}", text)
.map_err(|e| VmError::NativeError(std::format!("shell::writeln_err: {}", e)))?;
Ok(GenericValue::Unit)
}
fn pid_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if !args.is_empty() {
return Err(VmError::NativeError(std::string::String::from(
"shell::pid: expected zero arguments",
)));
}
Ok(GenericValue::Int(W::from_i64_wrap(
std::process::id() as i64
)))
}
fn hostname_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if !args.is_empty() {
return Err(VmError::NativeError(std::string::String::from(
"shell::hostname: expected zero arguments",
)));
}
let output = Command::new("hostname").output().map_err(|e| {
VmError::NativeError(std::format!(
"shell::hostname: failed to spawn hostname: {}",
e
))
})?;
if !output.status.success() {
return Err(VmError::NativeError(std::format!(
"shell::hostname: hostname command exited with code {}",
output.status.code().unwrap_or(-1)
)));
}
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim_end_matches(['\n', '\r']);
Ok(GenericValue::StaticStr(std::string::String::from(trimmed)))
}
fn arg_count_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if !args.is_empty() {
return Err(VmError::NativeError(std::string::String::from(
"shell::arg_count: expected zero arguments",
)));
}
Ok(GenericValue::Int(W::from_i64_wrap(script_arg_count())))
}
fn arg_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::arg: expected exactly one argument",
)));
}
let index = match args[0] {
GenericValue::Int(n) => W::to_i64(n),
ref v => {
return Err(VmError::TypeError(std::format!(
"shell::arg: expected Word, got {}",
v.type_name()
)));
}
};
if index < 0 {
return Ok(GenericValue::None);
}
let value = script_arg_at(index as usize);
match value {
Some(v) => Ok(GenericValue::Enum(crate::bytecode::EnumBody::boxed(
std::string::String::from("Option"),
std::string::String::from("Some"),
std::vec![GenericValue::StaticStr(v)],
))),
None => Ok(GenericValue::None),
}
}
fn setenv_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 2 {
return Err(VmError::NativeError(std::string::String::from(
"shell::setenv: expected exactly two arguments",
)));
}
let name = arg_str(ctx, args, 0, "shell::setenv: expected Text for name")?;
let name: &str = &name;
let value = arg_str(ctx, args, 1, "shell::setenv: expected Text for value")?;
let value: &str = &value;
unsafe {
std::env::set_var(name, value);
}
Ok(GenericValue::Unit)
}
fn pwd_native<W: Word, F: Float>(
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if !args.is_empty() {
return Err(VmError::NativeError(std::string::String::from(
"shell::pwd: expected zero arguments",
)));
}
let cwd = std::env::current_dir()
.map_err(|e| VmError::NativeError(std::format!("shell::pwd: {}", e)))?;
let s = cwd.to_string_lossy().into_owned();
Ok(GenericValue::StaticStr(s))
}
fn cd_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 1 {
return Err(VmError::NativeError(std::string::String::from(
"shell::cd: expected exactly one argument",
)));
}
let path = arg_str(ctx, args, 0, "shell::cd: expected Text")?;
let path: &str = &path;
std::env::set_current_dir(path)
.map_err(|e| VmError::NativeError(std::format!("shell::cd: {}: {}", path, e)))?;
Ok(GenericValue::Unit)
}
fn run_timeout_native<W: Word, F: Float>(
ctx: &NativeCtx<'_>,
args: &[GenericValue<W, F>],
) -> Result<GenericValue<W, F>, VmError> {
if args.len() != 2 {
return Err(VmError::NativeError(std::string::String::from(
"shell::run_timeout: expected exactly two arguments",
)));
}
let cmd = arg_str(ctx, args, 0, "shell::run_timeout: expected Text for cmd")?;
let cmd: &str = &cmd;
let ms = match args[1] {
GenericValue::Int(n) => W::to_i64(n),
ref v => {
return Err(VmError::TypeError(std::format!(
"shell::run_timeout: expected Word for ms, got {}",
v.type_name()
)));
}
};
if ms <= 0 {
return Err(VmError::NativeError(std::format!(
"shell::run_timeout: timeout must be positive, got {}",
ms
)));
}
let timeout = Duration::from_millis(ms as u64);
let mut child = Command::new("sh")
.arg("-c")
.arg(cmd)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| {
VmError::NativeError(std::format!(
"shell::run_timeout: failed to spawn sh: {}",
e
))
})?;
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(status)) => {
let output = child.wait_with_output().map_err(|e| {
VmError::NativeError(std::format!(
"shell::run_timeout: failed to collect output: {}",
e
))
})?;
let exit_code = status.code().unwrap_or(-1) as i64;
let stdout = std::string::String::from_utf8_lossy(&output.stdout).into_owned();
return Ok(GenericValue::tuple(std::vec![
GenericValue::Int(W::from_i64_wrap(exit_code)),
GenericValue::StaticStr(stdout),
]));
}
Ok(None) => {
if start.elapsed() >= timeout {
let _ = child.kill();
let _ = child.wait();
return Err(VmError::NativeError(std::format!(
"shell::run_timeout: command exceeded timeout of {}ms",
ms
)));
}
std::thread::sleep(Duration::from_millis(10));
}
Err(e) => {
return Err(VmError::NativeError(std::format!(
"shell::run_timeout: wait failed: {}",
e
)));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::string::ToString;
type V = GenericValue<i64, f64>;
fn test_ctx(arena: &keleusma_arena::Arena) -> NativeCtx<'_> {
NativeCtx {
arena,
opaques: &[],
word_bytes: 8,
float_bytes: 8,
}
}
#[test]
fn ctx_native_resolves_dynamic_kstr_argument() {
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let handle = crate::kstring::KString::alloc(&arena, "KELEUSMA_SHELL_KSTR_TEST_UNSET")
.expect("alloc kstr");
let result =
has_env_native::<i64, f64>(&ctx, &[V::KStr(handle)]).expect("has_env resolves KStr");
assert!(matches!(result, V::Bool(false)));
}
fn unwrap_some(v: V) -> std::string::String {
match v {
V::Enum(crate::bytecode::EnumBody::Boxed(b)) => {
assert_eq!(b.type_name, "Option");
assert_eq!(b.variant, "Some");
match &b.fields[..] {
[V::StaticStr(s)] => s.clone(),
other => panic!("unexpected Some payload: {:?}", other),
}
}
other => panic!("expected Option::Some, got {:?}", other),
}
}
#[test]
fn arg_reports_installed_script_vector() {
set_script_args(std::vec![
"script.kel".to_string(),
"alpha".to_string(),
"beta".to_string(),
]);
assert_eq!(
unwrap_some(arg_native::<i64, f64>(&[V::Int(0)]).unwrap()),
"script.kel"
);
assert_eq!(
unwrap_some(arg_native::<i64, f64>(&[V::Int(1)]).unwrap()),
"alpha"
);
assert_eq!(
unwrap_some(arg_native::<i64, f64>(&[V::Int(2)]).unwrap()),
"beta"
);
assert!(matches!(
arg_native::<i64, f64>(&[V::Int(3)]).unwrap(),
V::None
));
match arg_count_native::<i64, f64>(&[]).unwrap() {
V::Int(n) => assert_eq!(n, 3),
other => panic!("wrong variant: {:?}", other),
}
clear_script_args();
}
#[test]
fn arg_negative_index_is_none() {
set_script_args(std::vec!["script.kel".to_string(), "x".to_string()]);
assert!(matches!(
arg_native::<i64, f64>(&[V::Int(-1)]).unwrap(),
V::None
));
clear_script_args();
}
#[test]
fn clear_script_args_restores_process_argv_fallback() {
set_script_args(std::vec!["only".to_string()]);
match arg_count_native::<i64, f64>(&[]).unwrap() {
V::Int(n) => assert_eq!(n, 1),
other => panic!("wrong variant: {:?}", other),
}
clear_script_args();
match arg_count_native::<i64, f64>(&[]).unwrap() {
V::Int(n) => assert!(n >= 1),
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn run_full_returns_stdout_and_stderr() {
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let result = run_full_native::<i64, f64>(
&ctx,
&[V::StaticStr("printf out; printf err 1>&2".to_string())],
)
.expect("run_full");
match result {
V::Tuple(items) => {
let items = items.elements();
assert_eq!(items.len(), 3);
assert!(matches!(items[0], V::Int(0)));
match (&items[1], &items[2]) {
(V::StaticStr(out), V::StaticStr(err)) => {
assert_eq!(out, "out");
assert_eq!(err, "err");
}
other => panic!("unexpected tuple payload: {:?}", other),
}
}
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn run_full_propagates_exit_code() {
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let result = run_full_native::<i64, f64>(&ctx, &[V::StaticStr("exit 7".to_string())])
.expect("run_full");
match result {
V::Tuple(items) => assert!(matches!(items.elements()[0], V::Int(7))),
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn run_full_rejects_wrong_arity() {
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let err = run_full_native::<i64, f64>(&ctx, &[]).expect_err("arity");
match err {
VmError::NativeError(m) => assert!(m.contains("expected exactly one")),
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn sleep_ms_zero_returns_immediately() {
let result = sleep_ms_native::<i64, f64>(&[V::Int(0)]).expect("sleep_ms(0)");
assert!(matches!(result, V::Unit));
}
#[test]
fn sleep_ms_negative_returns_immediately() {
let result = sleep_ms_native::<i64, f64>(&[V::Int(-5)]).expect("sleep_ms(-5)");
assert!(matches!(result, V::Unit));
}
#[test]
fn sleep_ms_rejects_wrong_arity() {
let err = sleep_ms_native::<i64, f64>(&[]).expect_err("arity");
match err {
VmError::NativeError(m) => assert!(m.contains("expected exactly one")),
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn sleep_ms_rejects_non_word() {
let err = sleep_ms_native::<i64, f64>(&[V::Bool(true)]).expect_err("type");
assert!(matches!(err, VmError::TypeError(_)));
}
#[test]
fn now_unix_ms_positive() {
let result = now_unix_ms_native::<i64, f64>(&[]).expect("now_unix_ms()");
match result {
V::Int(n) => assert!(n > 1_700_000_000_000),
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn now_unix_ms_rejects_args() {
let err = now_unix_ms_native::<i64, f64>(&[V::Int(0)]).expect_err("args");
assert!(matches!(err, VmError::NativeError(_)));
}
#[test]
fn file_exists_true_and_false() {
let mut tmp_path = std::env::temp_dir();
tmp_path.push("keleusma_shell_test_exists.txt");
let path_str = tmp_path.to_string_lossy().to_string();
let _ = std::fs::remove_file(&tmp_path);
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let none = file_exists_native::<i64, f64>(&ctx, &[V::StaticStr(path_str.clone())])
.expect("file_exists");
assert!(matches!(none, V::Bool(false)));
std::fs::write(&tmp_path, b"").expect("write tmp");
let some = file_exists_native::<i64, f64>(&ctx, &[V::StaticStr(path_str.clone())])
.expect("file_exists");
assert!(matches!(some, V::Bool(true)));
let _ = std::fs::remove_file(&tmp_path);
}
#[test]
fn write_read_append_roundtrip() {
let mut tmp_path = std::env::temp_dir();
tmp_path.push("keleusma_shell_test_io.txt");
let path_str = tmp_path.to_string_lossy().to_string();
let _ = std::fs::remove_file(&tmp_path);
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
write_file_native::<i64, f64>(
&ctx,
&[
V::StaticStr(path_str.clone()),
V::StaticStr(std::string::String::from("first\n")),
],
)
.expect("write");
append_file_native::<i64, f64>(
&ctx,
&[
V::StaticStr(path_str.clone()),
V::StaticStr(std::string::String::from("second\n")),
],
)
.expect("append");
let result =
read_file_native::<i64, f64>(&ctx, &[V::StaticStr(path_str.clone())]).expect("read");
match result {
V::StaticStr(s) => assert_eq!(s, "first\nsecond\n"),
other => panic!("wrong variant: {:?}", other),
}
let _ = std::fs::remove_file(&tmp_path);
}
#[test]
fn read_file_traps_on_missing() {
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let err = read_file_native::<i64, f64>(
&ctx,
&[V::StaticStr(std::string::String::from(
"/nonexistent/keleusma_shell_test_missing.txt",
))],
)
.expect_err("missing");
match err {
VmError::NativeError(m) => assert!(m.contains("/nonexistent")),
other => panic!("wrong variant: {:?}", other),
}
}
#[test]
fn write_file_rejects_arity() {
let arena = keleusma_arena::Arena::with_capacity(4096);
let ctx = test_ctx(&arena);
let err =
write_file_native::<i64, f64>(&ctx, &[V::StaticStr(std::string::String::from("x"))])
.expect_err("arity");
match err {
VmError::NativeError(m) => assert!(m.contains("two")),
other => panic!("wrong variant: {:?}", other),
}
}
}