From f099dacdfe504843891128e528203fb618b92659 Mon Sep 17 00:00:00 2001 From: Steven Stanfield Date: Wed, 1 May 2024 00:26:39 -0400 Subject: [PATCH 1/4] WIP, checkpoint- all tests passing but still need a couple functions and cleanups. --- Cargo.lock | 11 +- Cargo.toml | 1 + bridge_adapters/src/lisp_adapters/numbers.rs | 5 + .../src/lisp_adapters/primitives.rs | 6 + bridge_adapters/src/lisp_adapters/text.rs | 1 + builtins/Cargo.toml | 6 + builtins/src/fs_meta.rs | 672 ++++++++++++++++++ builtins/src/fs_temp.rs | 230 ++++++ builtins/src/io.rs | 354 +++++++-- builtins/src/lib.rs | 3 + builtins/src/print.rs | 35 + builtins/src/rand.rs | 273 +++++++ compiler/src/compile/compile_let.rs | 3 - compiler/src/lib.rs | 4 + compiler/src/load_eval.rs | 25 +- lisp/core.slosh | 94 +++ slosh_lib/src/shell_builtins.rs | 2 +- slosh_test/Cargo.toml | 1 + slosh_test/src/docs.rs | 11 +- slosh_test/tests/slosh-tests.rs | 2 +- vm/src/heap/io.rs | 58 ++ vm/src/vm/storage.rs | 9 + 22 files changed, 1727 insertions(+), 79 deletions(-) create mode 100644 builtins/src/fs_meta.rs create mode 100644 builtins/src/fs_temp.rs create mode 100644 builtins/src/rand.rs diff --git a/Cargo.lock b/Cargo.lock index 778b785349..f64d7a3d00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,10 +256,16 @@ dependencies = [ "bridge_macros", "bridge_types", "compile_state", + "glob", + "rand 0.8.5", + "same-file", + "shell", "slvm", "static_assertions", "trybuild", "unicode-segmentation", + "unicode_reader", + "walkdir", ] [[package]] @@ -2065,6 +2071,7 @@ dependencies = [ "lazy_static", "mdbook", "regex", + "shell", "sl-compiler", "slosh_lib", "slvm", @@ -2503,9 +2510,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", diff --git a/Cargo.toml b/Cargo.toml index 5957cbbc31..257d8a7e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ compile_state = { path = "compile_state" } sl-compiler = { path = "compiler" } compiler_test_utils = { path = "compiler/test_utils" } slvm = { path = "vm" } +shell = { path = "shell" } regex = "1" lazy_static = "1" mdbook = "0.4.37" diff --git a/bridge_adapters/src/lisp_adapters/numbers.rs b/bridge_adapters/src/lisp_adapters/numbers.rs index 1f848823bc..75ec4b5c5a 100644 --- a/bridge_adapters/src/lisp_adapters/numbers.rs +++ b/bridge_adapters/src/lisp_adapters/numbers.rs @@ -35,6 +35,11 @@ impl SlFrom for Value { } } +impl SlFrom for Value { + fn sl_from(value: i64, _vm: &mut SloshVm) -> VMResult { + Ok(to_i56(value)) + } +} impl<'a> SlFromRef<'a, Value> for i32 { fn sl_from_ref(value: Value, vm: &'a SloshVm) -> VMResult { match value { diff --git a/bridge_adapters/src/lisp_adapters/primitives.rs b/bridge_adapters/src/lisp_adapters/primitives.rs index cc1d3bf44d..fd1acf5ab5 100644 --- a/bridge_adapters/src/lisp_adapters/primitives.rs +++ b/bridge_adapters/src/lisp_adapters/primitives.rs @@ -2,6 +2,12 @@ use crate::lisp_adapters::{SlFrom, SlFromRef}; use compile_state::state::SloshVm; use slvm::{VMResult, Value}; +impl<'a> SlFromRef<'a, Value> for Value { + fn sl_from_ref(value: Value, _vm: &SloshVm) -> VMResult { + Ok(value) + } +} + impl<'a> SlFromRef<'a, Value> for bool { fn sl_from_ref(value: Value, _vm: &SloshVm) -> VMResult { match value { diff --git a/bridge_adapters/src/lisp_adapters/text.rs b/bridge_adapters/src/lisp_adapters/text.rs index b2e6a8fdc2..d81d508055 100644 --- a/bridge_adapters/src/lisp_adapters/text.rs +++ b/bridge_adapters/src/lisp_adapters/text.rs @@ -166,6 +166,7 @@ impl<'a> SlFromRef<'a, Value> for String { fn sl_from_ref(value: Value, vm: &'a SloshVm) -> VMResult { match value { Value::String(h) => Ok(vm.get_string(h).to_string()), + Value::StringConst(i) => Ok(vm.get_interned(i).to_string()), _ => Err(VMError::new_conversion( ErrorStrings::fix_me_mismatched_type( <&'static str>::from(ValueType::String), diff --git a/builtins/Cargo.toml b/builtins/Cargo.toml index 20046e6cf9..5184ec500a 100644 --- a/builtins/Cargo.toml +++ b/builtins/Cargo.toml @@ -8,11 +8,17 @@ edition = "2021" [dependencies] slvm = { workspace = true } compile_state = { workspace = true } +shell = { workspace = true } bridge_adapters = { path = "../bridge_adapters" } unicode-segmentation = "1.10.1" +unicode_reader = "1" bridge_types = { workspace = true } bridge_macros = { path = "../bridge_macros" } static_assertions = "1.1.0" +rand = "0.8.5" +walkdir = "2.5.0" +same-file = "1.0.6" +glob = "0.3" [dev-dependencies] trybuild = "1.0" diff --git a/builtins/src/fs_meta.rs b/builtins/src/fs_meta.rs new file mode 100644 index 0000000000..e62761944f --- /dev/null +++ b/builtins/src/fs_meta.rs @@ -0,0 +1,672 @@ +use bridge_macros::sl_sh_fn; +use bridge_types::VarArgs; +use compile_state::state::SloshVm; +use shell::builtins::expand_tilde; +//use slvm::{from_i56, VMError, VMResult, Value}; +use slvm::{VMError, VMResult, Value}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::{env, fs, io}; + +use glob::glob; + +use bridge_adapters::add_builtin; +use same_file; +use std::fs::{File, Metadata}; +use std::time::SystemTime; +//use walkdir::{DirEntry, WalkDir}; + +fn cd_expand_all_dots(cd: String) -> String { + let mut all_dots = false; + if cd.len() > 2 { + all_dots = true; + for ch in cd.chars() { + if ch != '.' { + all_dots = false; + break; + } + } + } + if all_dots { + let mut new_cd = String::new(); + let paths_up = cd.len() - 2; + new_cd.push_str("../"); + for _i in 0..paths_up { + new_cd.push_str("../"); + } + new_cd + } else { + cd + } +} + +/// Usage: (cd dir-to-change-to) +/// +/// Change directory. +/// +/// Section: file +/// +/// Example: +/// (with-temp (fn (tmp) +/// (fclose (fopen (str tmp "/fs-cd-marker") :create :truncate)) +/// (test::assert-false (fs-exists? "fs-cd-marker")) +/// (cd tmp) +/// (test::assert-true (fs-exists? "fs-cd-marker")) +/// (cd))) +#[sl_sh_fn(fn_name = "cd")] +fn cd(arg: Option) -> VMResult { + let fn_name = "cd"; + let home = match env::var("HOME") { + Ok(val) => val, + Err(_) => "/".to_string(), + }; + let old_dir = match env::var("OLDPWD") { + Ok(val) => val, + Err(_) => home.to_string(), + }; + let new_dir = match arg { + Some(arg) => expand_tilde(arg.into()).to_string_lossy().to_string(), + None => home, + }; + let new_dir = if new_dir == "-" { &old_dir } else { &new_dir }; + let new_dir = cd_expand_all_dots(new_dir.to_string()); + let root = Path::new(&new_dir); + if let Ok(oldpwd) = env::current_dir() { + env::set_var("OLDPWD", oldpwd); + } + if let Err(e) = env::set_current_dir(root) { + eprintln!("{} Error changing to {}, {}", fn_name, root.display(), e); + Ok(Value::Nil) + } else { + env::set_var("PWD", env::current_dir()?); + Ok(Value::True) + } +} + +pub fn get_file(p: &str) -> Option { + let p = expand_tilde(p.into()); + Some(p.to_path_buf()) +} + +fn file_test(path: &str, test: fn(path: &Path) -> bool, fn_name: &str) -> VMResult { + if let Some(path) = get_file(path) { + if test(path.as_path()) { + Ok(Value::True) + } else { + Ok(Value::False) + } + } else { + let msg = format!("{} takes a string (a path)", fn_name); + Err(VMError::new("io", msg)) + } +} + +/// Usage: (fs-exists? path-to-test) +/// +/// Does the given path exist? +/// +/// Section: file +/// +/// Example: +/// (with-temp (fn (tmp) +/// (fclose (fopen (str tmp "/fs-exists") :create :truncate)) +/// (test::assert-true (fs-exists? (str tmp "/fs-exists"))) +/// (test::assert-true (fs-exists? tmp)) +/// (test::assert-false (fs-exists? (str tmp "/fs-exists-nope"))))) +#[sl_sh_fn(fn_name = "fs-exists?")] +fn path_exists(path: &str) -> VMResult { + file_test(path, |path| path.exists(), "fs-exists?") +} + +/// Usage: (fs-file? path-to-test) +/// +/// Is the given path a file? +/// +/// Section: file +/// +/// Example: +/// (with-temp (fn (tmp) +/// (fclose (fopen (str tmp "/fs-file") :create :truncate)) +/// (test::assert-true (fs-file? (str tmp "/fs-file"))) +/// (test::assert-false (fs-file? tmp)) +/// (test::assert-false (fs-file? (str tmp "/fs-file-nope"))))) +#[sl_sh_fn(fn_name = "fs-file?")] +fn is_file(path: &str) -> VMResult { + file_test(path, |path| path.is_file(), "fs-file?") +} + +/// Usage: (fs-dir? path-to-test) +/// +/// Is the given path a directory? +/// +/// Section: file +/// +/// Example: +/// (with-temp (fn (tmp) +/// (fclose (fopen (str tmp "/fs-dir-file") :create :truncate)) +/// (test::assert-false (fs-dir? (str tmp "/fs-dir-file"))) +/// (test::assert-true (fs-dir? tmp)) +/// (test::assert-false (fs-file? (str tmp "/fs-dir-nope"))))) +#[sl_sh_fn(fn_name = "fs-dir?")] +fn is_dir(path: &str) -> VMResult { + file_test(path, |path| path.is_dir(), "fs-dir?") +} + +/// Usage: (glob /path/with/*) +/// +/// Takes a list/varargs of globs and return the list of them expanded. +/// +/// Section: file +/// +/// Example: +/// (with-temp (fn (tmp) +/// (fclose (fopen (str tmp "/g1") :create :truncate)) +/// (fclose (fopen (str tmp "/g2") :create :truncate)) +/// (fclose (fopen (str tmp "/g3") :create :truncate)) +/// (test::assert-equal [(str tmp "/g1") (str tmp "/g2") (str tmp "/g3")] (glob (str tmp "/*"))))) +#[sl_sh_fn(fn_name = "glob", takes_env = true)] +fn do_glob(environment: &mut SloshVm, args: VarArgs) -> VMResult { + fn remove_escapes(pat: &str) -> String { + let mut ret = String::new(); + let mut last_esc = false; + for ch in pat.chars() { + match ch { + '\\' if last_esc => { + ret.push('\\'); + last_esc = false; + } + '\\' => last_esc = true, + '*' if last_esc => { + ret.push('*'); + last_esc = false; + } + '?' if last_esc => { + ret.push('?'); + last_esc = false; + } + '[' if last_esc => { + ret.push('['); + last_esc = false; + } + ']' if last_esc => { + ret.push(']'); + last_esc = false; + } + _ => { + if last_esc { + ret.push('\\'); + } + ret.push(ch); + } + } + } + ret + } + let mut files = Vec::new(); + for pat in args { + let pat = expand_tilde(pat.into()).to_string_lossy().to_string(); + if let Ok(paths) = glob(&pat) { + for p in paths { + match p { + Ok(p) => { + if let Some(p) = p.to_str() { + files.push(environment.alloc_string(p.to_string())); + } + } + Err(err) => { + let msg = format!("glob error on while iterating {}, {}", pat, err); + return Err(VMError::new("io", msg)); + } + } + } + if files.is_empty() { + // Got nothing so fall back on pattern. + if pat.contains('\\') { + files.push(environment.alloc_string(remove_escapes(&pat))); + } else { + files.push(environment.alloc_string(pat)); + } + } + } else if pat.contains('\\') { + files.push(environment.alloc_string(remove_escapes(&pat))); + } else { + files.push(environment.alloc_string(pat)); + } + } + Ok(environment.alloc_vector(files)) +} + +/// Usage: (fs-parent /path/to/file/or/dir) +/// +/// Returns base name of file or directory passed to function. +/// +/// Section: file +/// Example: +/// (with-temp (fn (tmp) +/// (let ((tmp-file (get-temp-file tmp))) +/// (test::assert-true (fs-same? (fs-parent tmp-file) tmp))))) +#[sl_sh_fn(fn_name = "fs-parent")] +fn fs_parent(path: &str) -> VMResult { + let fn_name = "fs-parent"; + if let Some(path) = get_file(path) { + let mut path = path.canonicalize().map_err(|_| { + let msg = format!("{} failed to get full filepath of parent", fn_name); + VMError::new("io", msg) + })?; + let _ = path.pop(); + let path = path.as_path().to_str().ok_or_else(|| { + let msg = format!("{} failed to get parent path", fn_name); + VMError::new("io", msg) + })?; + Ok(path.to_string()) + } else { + let msg = format!("{} first arg is not a valid path", fn_name); + Err(VMError::new("io", msg)) + } +} + +/// Usage: (fs-base /path/to/file/or/dir) +/// +/// Returns base name of file or directory passed to function. +/// +/// Section: file +/// Example: +/// (with-temp (fn (tmp) +/// (let ((tmp-file (temp-file tmp))) +/// (test::assert-equal (length \".tmp01234\") (length (fs-base tmp-file)))))) +#[sl_sh_fn(fn_name = "fs-base")] +fn fs_base(path: &str) -> VMResult { + let fn_name = "fs-base"; + match get_file(path) { + Some(path) => { + let path = path.file_name().and_then(|s| s.to_str()).ok_or_else(|| { + let msg = format!("{} failed to extract name of file", fn_name); + VMError::new("io", msg) + })?; + Ok(path.to_string()) + } + None => { + let msg = format!("{} first arg is not a valid path", fn_name); + Err(VMError::new("io", msg)) + } + } +} + +/// Usage: (fs-same? /path/to/file/or/dir /path/to/file/or/dir) +/// +/// Returns true if the two provided file paths refer to the same file or directory. +/// +/// Section: file +/// +/// Example: +/// (with-temp-file (fn (tmp-file) +/// (test::assert-true (fs-same? tmp-file tmp-file)))) +#[sl_sh_fn(fn_name = "fs-same?")] +fn is_same_file(path_0: &str, path_1: &str) -> VMResult { + let fn_name = "fs-same?"; + match (get_file(path_0), get_file(path_1)) { + (Some(path_0), Some(path_1)) => { + if let Ok(b) = same_file::is_same_file(path_0.as_path(), path_1.as_path()) { + if b { + Ok(Value::True) + } else { + Ok(Value::False) + } + } else { + let msg = format!( + "{} there were insufficient permissions to access one or both of the provided files.", + fn_name + ); + Err(VMError::new("io", msg)) + } + } + (_, _) => { + let msg = format!("{} one or more paths does not exist.", fn_name); + Err(VMError::new("io", msg)) + } + } +} + +/* +/// Usage: (fs-crawl /path/to/file/or/dir (fn (x) (println "found path" x) [max-depth] +/// [:follow-syms]) +/// +/// If a directory is provided the path is recursively searched and every +/// file and directory is called as an argument to the provided function. +/// If a file is provided the path is provided as an argument to the provided +/// function. Takes two optional arguments (in any order) an integer, +/// representing max depth to traverse if file is a directory, or the +/// symbol, :follow-syms, to follow symbol links when traversing if +/// desired. +/// +/// +/// Section: file +/// +/// Example: +/// +/// (with-temp-file (fn (tmp-file) +/// (def cnt 0) +/// (fs-crawl tmp-file (fn (x) +/// (test::assert-equal (fs-base tmp-file) (fs-base x)) +/// (set! cnt (+ 1 cnt)))) +/// (test::assert-equal 1 cnt))) +/// +/// (defn create-in (in-dir num-files visited) +/// (dotimes-i i num-files +/// (hash-set! visited (get-temp-file in-dir) nil))) +/// +/// (defn create-dir (tmp-dir visited) +/// (let ((new-tmp (get-temp tmp-dir))) +/// (hash-set! visited new-tmp nil) +/// new-tmp)) +/// +/// (with-temp (fn (root-tmp-dir) +/// (let ((tmp-file-count 5) +/// (visited (make-hash))) +/// (def cnt 0) +/// (hash-set! visited root-tmp-dir nil) +/// (create-in root-tmp-dir tmp-file-count visited) +/// (let* ((tmp-dir (create-dir root-tmp-dir visited)) +/// (new-files (create-in tmp-dir tmp-file-count visited)) +/// (tmp-dir (create-dir tmp-dir visited)) +/// (new-files (create-in tmp-dir tmp-file-count visited))) +/// (fs-crawl root-tmp-dir (fn (x) +/// (let ((file (hash-get visited x))) +/// (test::assert-true (not file)) ;; also tests double counting +/// (hash-set! visited x #t) +/// (set! cnt (+ 1 cnt))))) +/// (test::assert-equal (+ 3 (* 3 tmp-file-count)) cnt) +/// (test::assert-equal (+ 3 (* 3 tmp-file-count)) (length (hash-keys visited))) +/// (iterator::map (fn (x) (test::assert-true (hash-get visited y))) (hash-keys visited)))))) +/// +/// (with-temp (fn (root-tmp-dir) +/// (let ((tmp-file-count 5) +/// (visited (make-hash))) +/// (def cnt 0) +/// (hash-set! visited root-tmp-dir nil) +/// (create-in root-tmp-dir tmp-file-count visited) +/// (let* ((tmp-dir (create-dir root-tmp-dir visited)) +/// (new-files (create-in tmp-dir tmp-file-count visited)) +/// (tmp-dir (create-dir tmp-dir (make-hash))) +/// (new-files (create-in tmp-dir tmp-file-count (make-hash)))) +/// (fs-crawl root-tmp-dir (fn (x) +/// (let ((file (hash-get visited x))) +/// (test::assert-true (not file)) ;; also tests double counting +/// (hash-set! visited x #t) +/// (set! cnt (+ 1 cnt)))) 2) +/// (test::assert-equal (+ 3 (* 2 tmp-file-count)) cnt) +/// (test::assert-equal (+ 3 (* 2 tmp-file-count)) (length (hash-keys visited))) +/// (iterator::map (fn (x) (test::assert-true (hash-get visited y))) (hash-keys visited)))))) +/// +/// (with-temp (fn (root-tmp-dir) +/// (let ((tmp-file-count 5) +/// (visited (make-hash))) +/// (def cnt 0) +/// (hash-set! visited root-tmp-dir nil) +/// (create-in root-tmp-dir tmp-file-count visited) +/// (let* ((tmp-dir (create-dir root-tmp-dir (make-hash))) +/// (new-files (create-in tmp-dir tmp-file-count (make-hash))) +/// (tmp-dir (create-dir tmp-dir (make-hash))) +/// (new-files (create-in tmp-dir tmp-file-count (make-hash)))) +/// (fs-crawl root-tmp-dir (fn (x) +/// (let ((file (hash-get visited x))) +/// (test::assert-true (not file)) ;; also tests double counting +/// (hash-set! visited x #t) +/// (set! cnt (+ 1 cnt)))) 1) +/// (test::assert-equal (+ 2 tmp-file-count) cnt) +/// (test::assert-equal (+ 2 tmp-file-count) (length (hash-keys visited))) +/// (iterator::map (fn (x) (test::assert-true (hash-get visited y))) (hash-keys visited)))))) +#[sl_sh_fn(fn_name = "fs-crawl", takes_env = true)] +fn fs_crawl( + environment: &mut SloshVm, + path: String, + lambda_exp: Value, + optional_depth_or_symlink: VarArgs, +) -> VMResult { + let fn_name = "fs-crawl"; + let file_or_dir = get_file(&path); + let mut depth = None; + let mut sym_links = None; + for depth_or_symlink in optional_depth_or_symlink { + match depth_or_symlink { + Value::Int(i) => { + let i: i64 = from_i56(&i); + depth = Some(i) + } + Value::Keyword(i) if environment.get_interned(i) == "follow-syms" => { + sym_links = Some(true); + } + _ => return Err(VMError::new("io", format!("invalid argument {}", depth_or_symlink.display_value(environment)))), + } + } + match lambda_exp { + Value::Lambda(_) | Value::Closure(_) => { + if let Some(file_or_dir) = file_or_dir { + let mut cb = |entry: &DirEntry| -> VMResult<()> { + let path = entry.path(); + if let Some(path) = path.to_str() { + let path = environment.alloc_string(path.to_string()); + // XXXX make a call... + //call_lambda(environment, lambda_exp, &[path])?; + } + Ok(()) + }; + match (depth, sym_links) { + (Some(depth), Some(sym_links)) => { + for entry in WalkDir::new(file_or_dir) + .max_depth(depth as usize) + .follow_links(sym_links) + .into_iter() + .filter_map(|e| e.ok()) + { + cb(&entry)?; + } + } + (Some(depth), None) => { + for entry in WalkDir::new(file_or_dir) + .max_depth(depth as usize) + .into_iter() + .filter_map(|e| e.ok()) + { + cb(&entry)?; + } + } + (None, Some(sym_links)) => { + for entry in WalkDir::new(file_or_dir) + .follow_links(sym_links) + .into_iter() + .filter_map(|e| e.ok()) + { + cb(&entry)?; + } + } + (None, None) => { + for entry in WalkDir::new(file_or_dir).into_iter().filter_map(|e| e.ok()) { + cb(&entry)?; + } + } + } + Ok(Value::True) + } else { + let msg = format!("{} provided path does not exist", fn_name); + Err(VMError::new("io", msg)) + } + } + _ => { + let msg = format!("{} second argument must be a lambda", fn_name); + Err(VMError::new("io", msg)) + } + } +} + */ + +/// Usage: (fs-len /path/to/file/or/dir) +/// +/// Returns the size of the file in bytes. +/// +/// Section: file +/// +/// Example: +/// (with-temp-file (fn (tmp) +/// (let (tst-file (fopen tmp :create :truncate)) +/// (fprn tst-file "Test Line Read Line One") +/// (fpr tst-file "Test Line Read Line Two") +/// (fclose tst-file) +/// (test::assert-equal 47 (fs-len tmp))))) +#[sl_sh_fn(fn_name = "fs-len")] +fn fs_len(file_or_dir: &str) -> VMResult { + let fn_name = "fs-len"; + let file_or_dir = get_file(file_or_dir); + if let Some(file_or_dir) = file_or_dir { + if let Ok(metadata) = fs::metadata(file_or_dir) { + let len = metadata.len(); + Ok(len as i64) + } else { + let msg = format!("{} can not fetch metadata at provided path", fn_name); + Err(VMError::new("io", msg)) + } + } else { + let msg = format!("{} provided path does not exist", fn_name); + Err(VMError::new("io", msg)) + } +} + +fn get_file_time( + file_or_dir: Option, + fn_name: &str, + to_time: fn(Metadata) -> io::Result, +) -> VMResult { + if let Some(file_or_dir) = file_or_dir { + if let Ok(metadata) = fs::metadata(file_or_dir) { + if let Ok(sys_time) = to_time(metadata) { + match sys_time.duration_since(SystemTime::UNIX_EPOCH) { + Ok(n) => { + let n = n.as_millis() as i64; + Ok(n) + } + Err(_) => { + let msg = format!("{} can not parse time", fn_name); + Err(VMError::new("io", msg)) + } + } + } else { + let msg = format!("{} can not fetch time", fn_name); + Err(VMError::new("io", msg)) + } + } else { + let msg = format!("{} can not fetch metadata at provided path", fn_name); + Err(VMError::new("io", msg)) + } + } else { + let msg = format!("{} provided path does not exist", fn_name); + Err(VMError::new("io", msg)) + } +} + +/// Usage: (fs-modified /path/to/file/or/dir) +/// +/// Returns the unix time file last modified in ms. +/// +/// Section: file +/// +/// Example: +/// (with-temp-file (fn (tmp) +/// (let (tst-file (fopen tmp :create :truncate) +/// last-mod (fs-modified tmp)) +/// (fprn tst-file "Test Line Read Line One") +/// (fpr tst-file "Test Line Read Line Two") +/// (fflush tst-file) +/// (fclose tst-file) +/// (test::assert-true (>= (fs-modified tmp) last-mod))))) +#[sl_sh_fn(fn_name = "fs-modified")] +fn fs_modified(file_or_dir: &str) -> VMResult { + let file_or_dir = get_file(file_or_dir); + get_file_time(file_or_dir, "fs-modified", |md| md.modified()) +} + +/// Usage: (fs-accessed /path/to/file/or/dir) +/// +/// Returns the unix time file last accessed in ms. +/// +/// Section: file +/// +/// Example: +/// (with-temp-file (fn (tmp) +/// (let (tst-file (fopen tmp :create) +/// last-acc (fs-accessed tmp)) +/// (fclose tst-file) +/// (let (tst-file (fopen tmp :read)) +/// (test::assert-true (>= (fs-accessed tmp) last-acc)) +/// (fclose tst-file))))) +#[sl_sh_fn(fn_name = "fs-accessed")] +fn fs_accessed(file_or_dir: &str) -> VMResult { + let file_or_dir = get_file(file_or_dir); + get_file_time(file_or_dir, "fs-accessed", |md| md.accessed()) +} + +fn fs_meta(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut i = registers.iter(); + if let (Some(string), None) = (i.next(), i.next()) { + let name = string.pretty_value(vm); + let file = File::open(name)?; + let meta = file.metadata()?; + let mut map = HashMap::new(); + let ftype = if meta.is_dir() { + "dir" + } else if meta.is_file() { + "file" + } else if meta.is_symlink() { + "symlink" + } else { + "unknown" + }; + let ro = if meta.permissions().readonly() { + Value::True + } else { + Value::False + }; + map.insert(Value::Keyword(vm.intern_static("readonly")), ro); + map.insert( + Value::Keyword(vm.intern_static("len")), + (meta.len() as i64).into(), + ); + map.insert( + Value::Keyword(vm.intern_static("type")), + Value::Keyword(vm.intern_static(ftype)), + ); + // XXX TODO- include times. + Ok(vm.alloc_map(map)) + } else { + Err(VMError::new( + "io", + "fs-meta: takes a filename as only arg".to_string(), + )) + } +} + +pub fn add_fs_meta_builtins(env: &mut SloshVm) { + intern_cd(env); + intern_path_exists(env); + intern_is_file(env); + intern_is_dir(env); + intern_do_glob(env); + //intern_fs_crawl(env); + intern_is_same_file(env); + intern_fs_base(env); + intern_fs_parent(env); + intern_fs_len(env); + intern_fs_modified(env); + intern_fs_accessed(env); + + add_builtin( + env, + "fs-meta", + fs_meta, + r#"Usage: (fs-meta [FILENAME]) -> map + +Returns a map of a files meta data. + +Section: io +"#, + ); +} diff --git a/builtins/src/fs_temp.rs b/builtins/src/fs_temp.rs new file mode 100644 index 0000000000..04f1b885ba --- /dev/null +++ b/builtins/src/fs_temp.rs @@ -0,0 +1,230 @@ +use crate::rand::rand_alphanumeric_str; +use bridge_macros::sl_sh_fn; +use compile_state::state::SloshVm; +use rand; +use shell::builtins::expand_tilde; +use slvm::{VMError, VMResult}; +use std::ffi::OsStr; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +const LEN: i64 = 5; + +/// Usage: (get-temp ["/path/to/directory/to/use/as/base" "optional-prefix" "optional-suffix" length]) +/// +/// Creates a directory inside of an OS specific temporary directory. See [temp-dir](root::temp-dir) +/// for OS specific notes. Also accepts an optional prefix, an optional suffix, and an optional +/// length for the random number of characters in the temporary file created. Defaults to prefix of +/// ".tmp", no suffix, and five random characters. +/// +/// Section: file +/// +/// Example: +/// (test::assert-true (str-contains (get-temp) (temp-dir))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-dir (get-temp tmp)) +/// (test::assert-true (str-contains tmp-dir tmp))))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-dir (get-temp tmp "some-prefix")) +/// (test::assert-true (str-contains tmp-dir tmp)) +/// (test::assert-true (str-contains tmp-dir "some-prefix"))))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-dir (get-temp tmp "some-prefix" "some-suffix")) +/// (test::assert-true (str-contains tmp-dir tmp)) +/// (test::assert-true (str-contains tmp-dir "some-prefix")) +/// (test::assert-true (str-contains tmp-dir "some-suffix"))))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-dir (get-temp tmp "some-prefix" "some-suffix" 6)) +/// (test::assert-true (str-contains tmp-dir tmp)) +/// (test::assert-true (str-contains tmp-dir "some-prefix")) +/// (test::assert-true (str-contains tmp-dir "some-suffix")) +/// (test::assert-equal (len "some-prefix012345some-suffix") (len (fs-base tmp-dir)))))) +#[sl_sh_fn(fn_name = "get-temp")] +fn get_temp( + path: Option, + prefix: Option, + suffix: Option, + len: Option, +) -> VMResult { + let fn_name = "get-temp"; + let dir = get_provided_or_default_temp(path, fn_name)?; + let prefix = prefix.as_deref().unwrap_or_default(); + let suffix = suffix.as_deref().unwrap_or_default(); + let len = len.unwrap_or(LEN); + let dir = create_temp_dir(dir.as_path(), prefix, suffix, len, fn_name)?; + if let Some(path) = dir.to_str() { + let path = path.to_string(); + Ok(path) + } else { + let msg = format!("{} unable to provide temporary directory", fn_name); + Err(VMError::new("io", msg)) + } +} + +/// Usage: (temp-dir) +/// +/// Returns a string representing the temporary directory. See [get-temp](root::get-temp) for higher +/// level temporary directory creation mechanism. +/// +/// On Unix: +/// Returns the value of the TMPDIR environment variable if it is set, otherwise for non-Android it +/// returns /tmp. If Android, since there is no global temporary folder (it is usually allocated +/// per-app), it returns /data/local/tmp. +/// +/// On Windows: +/// Returns the value of, in order, the TMP, TEMP, USERPROFILE environment variable if any are set and +/// not the empty string. Otherwise, temp_dir returns the path of the Windows directory. This behavior +/// is identical to that of GetTempPath, which this function uses internally. +/// +/// Section: file +/// +/// Example: +/// (test::assert-true (fs-dir? (temp-dir))) +#[sl_sh_fn(fn_name = "temp-dir")] +fn builtin_temp_dir() -> VMResult { + if let Some(path) = temp_dir().to_str() { + Ok(path.to_string()) + } else { + Err(VMError::new( + "io", + "temp-dir: unable to provide temporary directory".to_string(), + )) + } +} + +fn get_provided_or_default_temp(path: Option, fn_name: &str) -> VMResult { + match path { + None => Ok(temp_dir()), + Some(path) => { + let dir = expand_tilde(path.into()); + let p = dir.as_path(); + if p.exists() && p.is_dir() { + Ok(dir) + } else { + let msg = format!( + "{} unable to provide temporary file in provided directory", + fn_name + ); + Err(VMError::new("io", msg)) + } + } + } +} + +fn create_temp_dir( + path: &Path, + prefix: &str, + suffix: &str, + len: i64, + fn_name: &str, +) -> VMResult { + if path.exists() && path.is_dir() { + let dir_name = random_name(prefix, suffix, len); + let dir = Path::new::(dir_name.as_ref()); + let dir = path.join(dir); + fs::create_dir(dir.as_path()).map_err(|err| { + let msg = format!("{} unable to create temporary directory inside default temporary directory ({:?}), reason: {:?}", fn_name, dir.as_path(), err); + VMError::new("io", msg) + })?; + Ok(dir) + } else { + let msg = format!("{} unable to provide temporary directory", fn_name); + Err(VMError::new("io", msg)) + } +} + +fn random_name(prefix: &str, suffix: &str, len: i64) -> String { + let prefix = if prefix.is_empty() { ".tmp" } else { prefix }; + let mut rng = rand::thread_rng(); + let name = rand_alphanumeric_str(len.unsigned_abs(), &mut rng); + format!("{}{}{}", prefix, name, suffix) +} + +fn temp_dir() -> PathBuf { + env::temp_dir() +} + +fn create_temp_file( + dir: PathBuf, + prefix: &Option, + suffix: &Option, + len: Option, + fn_name: &str, +) -> VMResult { + let prefix = prefix.as_ref().map(|x| x.as_str()).unwrap_or_default(); + let suffix = suffix.as_ref().map(|x| x.as_str()).unwrap_or_default(); + let len = len.unwrap_or(LEN); + let filename = random_name(prefix, suffix, len); + let p = Path::new::(filename.as_ref()); + let file = dir.join(p); + let p = file.as_path(); + File::create(p).map_err(|err| { + let msg = format!( + "{} unable to create temporary file inside temporary directory ({:?}), reason: {:?}", + fn_name, + dir.as_path(), + err + ); + VMError::new("io", msg) + })?; + Ok(file) +} + +/// Usage: (get-temp-file ["/path/to/directory/to/use/as/base" "optional-prefix" "optional-suffix" length]) +/// +/// Returns name of file created inside temporary directory. Optionally takes a directory to use as +/// the parent directory of the temporary file. Also accepts an optional prefix, an optional suffix, +/// and an optional length for the random number of characters in the temporary files created. Defaults +/// to prefix of ".tmp", no suffix, and five random characters. +/// +/// Section: file +/// +/// Example: +/// (test::assert-true (str-contains (get-temp-file) (temp-dir))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-file (get-temp-file tmp)) +/// (test::assert-true (str-contains tmp-file tmp))))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-file (get-temp-file tmp "some-prefix")) +/// (test::assert-true (str-contains tmp-file "some-prefix"))))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-file (get-temp-file tmp "some-prefix" "some-suffix")) +/// (test::assert-true (str-contains tmp-file "some-prefix")) +/// (test::assert-true (str-contains tmp-file "some-suffix"))))) +/// +/// (with-temp (fn (tmp) +/// (let (tmp-file (get-temp-file tmp "some-prefix" "some-suffix" 10)) +/// (test::assert-true (str-contains tmp-file "some-prefix")) +/// (test::assert-true (str-contains tmp-file "some-suffix")) +/// (test::assert-equal (len "some-prefix0123456789some-suffix") (len (fs-base tmp-file)))))) +#[sl_sh_fn(fn_name = "get-temp-file")] +fn get_temp_file( + path: Option, + prefix: Option, + suffix: Option, + len: Option, +) -> VMResult { + let fn_name = "get-temp-file"; + let dir = get_provided_or_default_temp(path, fn_name)?; + let file = create_temp_file(dir, &prefix, &suffix, len, fn_name)?; + if let Some(path) = file.as_path().to_str() { + Ok(path.to_string()) + } else { + let msg = format!("{} unable to provide temporary file", fn_name); + Err(VMError::new("io", msg)) + } +} + +pub fn add_fs_temp_builtins(env: &mut SloshVm) { + intern_get_temp(env); + intern_get_temp_file(env); + intern_builtin_temp_dir(env); +} diff --git a/builtins/src/io.rs b/builtins/src/io.rs index fc0a434425..795e3fa8a4 100644 --- a/builtins/src/io.rs +++ b/builtins/src/io.rs @@ -1,97 +1,329 @@ use bridge_adapters::add_builtin; use compile_state::state::SloshVm; use slvm::{VMError, VMResult, Value}; -use std::collections::HashMap; -use std::fs::File; -use std::path::Path; - -fn fs_meta(vm: &mut SloshVm, registers: &[Value]) -> VMResult { - let mut i = registers.iter(); - if let (Some(string), None) = (i.next(), i.next()) { - let name = string.pretty_value(vm); - let file = File::open(name)?; - let meta = file.metadata()?; - let mut map = HashMap::new(); - let ftype = if meta.is_dir() { - "dir" - } else if meta.is_file() { - "file" - } else if meta.is_symlink() { - "symlink" - } else { - "unknown" +use std::borrow::Cow; +use std::fs::{self, OpenOptions}; +use std::io::{Seek, SeekFrom, Write}; +extern crate unicode_reader; +use bridge_macros::sl_sh_fn; +use shell::builtins::expand_tilde; +use slvm::io::HeapIo; +use unicode_reader::Graphemes; + +fn fopen(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let Some(a) = args.next() { + if let Value::Keyword(sym) = a { + let ret = match vm.get_interned(*sym) { + "stdin" => Some(HeapIo::stdin()), + "stdout" => Some(HeapIo::stdout()), + "stderr" => Some(HeapIo::stderr()), + _ => None, + }; + if let Some(ret) = ret { + if args.next().is_some() { + return Err(VMError::new( + "io", + "fopen: if first form is a symbol then other forms not valid".to_string(), + )); + } + return Ok(vm.alloc_io(ret)); + } + } + let file_name = match a { + Value::String(h) => vm.get_string(*h), + Value::StringConst(i) => vm.get_interned(*i), + _ => { + return Err(VMError::new("io", "fopen: first form must evaluate to a string (filename) or :stdin, :stdout, :stderr")); + } + }; + let file_name = expand_tilde(file_name.into()); + let mut opts = OpenOptions::new(); + let mut is_read = false; + let mut is_write = false; + let mut error_nil = false; + for a in args { + if let Value::Keyword(i) = a { + match vm.get_interned(*i) { + "read" => { + is_read = true; + opts.read(true); + } + "write" => { + is_write = true; + opts.write(true); + } + "append" => { + is_write = true; + opts.append(true); + } + "truncate" => { + is_write = true; + opts.write(true); + opts.truncate(true); + } + "create" => { + is_write = true; + opts.write(true); + opts.create(true); + } + "create-new" => { + is_write = true; + opts.write(true); + opts.create_new(true); + } + "on-error-nil" => { + error_nil = true; + } + _ => { + let msg = format!("open: invalid directive, {}", vm.get_interned(*i)); + return Err(VMError::new("io", msg)); + } + }; + } else { + let msg = format!("fopen: {} invalid", a.display_type(vm)); + return Err(VMError::new("io", msg)); + } + } + if is_read && is_write { + return Err(VMError::new( + "io", + "fopen: only open file for read or write not both", + )); + } + if !is_write { + opts.read(true); + } + let file = match opts.open(&file_name) { + Ok(file) => file, + Err(err) => { + if error_nil { + return Ok(Value::Nil); + } else { + return Err(VMError::new( + "io", + format!("fopen: Error opening {}: {}", file_name.display(), err), + )); + } + } }; - let ro = if meta.permissions().readonly() { - Value::True + return if !is_write { + let io = HeapIo::from_file(file); + io.to_buf_reader() + .expect("Could not create a buf reader for file open to read!"); + Ok(vm.alloc_io(io)) + /*let fd: i64 = file.as_raw_fd() as i64; + let file_iter: CharIter = Box::new( + Graphemes::from(BufReader::new(file)) + .map(|s| { + if let Ok(s) = s { + Cow::Owned(s) + } else { + Cow::Borrowed("") + } + }) + .peekable(), + ); + Ok(Expression::alloc_data(ExpEnum::File(Rc::new( + RefCell::new(FileState::Read(Some(file_iter), fd)), + ))))*/ } else { - Value::False + let io = HeapIo::from_file(file); + io.to_buf_writer() + .expect("Could not create a buf writer for file open to write!"); + Ok(vm.alloc_io(io)) }; - map.insert(Value::Keyword(vm.intern_static("readonly")), ro); - map.insert( - Value::Keyword(vm.intern_static("len")), - (meta.len() as i64).into(), - ); - map.insert( - Value::Keyword(vm.intern_static("type")), - Value::Keyword(vm.intern_static(ftype)), - ); - // XXX TODO- include times. - Ok(vm.alloc_map(map)) - } else { - Err(VMError::new( - "io", - "fs-meta: takes a filename as only arg".to_string(), - )) } + Err(VMError::new( + "io", + "fopen takes at least one form (a file name)", + )) } -fn fs_exists(vm: &mut SloshVm, registers: &[Value]) -> VMResult { - let mut i = registers.iter(); - if let (Some(string), None) = (i.next(), i.next()) { - let string = string.pretty_value(vm); - let p = Path::new(&string); - if p.exists() { +fn fclose(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let (Some(exp), None) = (args.next(), args.next()) { + return if let Value::Io(h) = exp { + let io = vm.get_io(*h); + io.close(); Ok(Value::True) } else { - Ok(Value::False) + Err(VMError::new("io", "fclose requires a file")) + }; + } + Err(VMError::new("io", "fclose takes one form (file to close)")) +} + +fn builtin_read_line(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let (Some(Value::Io(h)), None) = (args.next(), args.next()) { + let mut io = vm.get_io(*h).get_io(); + let io_len = io.stream_position()?; + let mut f_iter: Box>> = + Box::new(Graphemes::from(io).map(|s| { + if let Ok(s) = s { + Cow::Owned(s) + } else { + Cow::Borrowed("") + } + })); + let mut line = String::new(); + let mut out_ch = f_iter.next(); + if out_ch.is_none() { + return Ok(Value::Nil); } + while let Some(ch) = out_ch { + line.push_str(&ch); + if ch == "\n" { + break; + } + out_ch = f_iter.next(); + } + drop(f_iter); + // Graphemes will pre-read (apparently) so reset the pos when done. + vm.get_io(*h) + .get_io() + .seek(SeekFrom::Start(io_len + line.len() as u64))?; + Ok(vm.alloc_string(line)) + } else { + Err(VMError::new("io", "read-line takes one form (file)")) + } +} + +fn builtin_flush(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let (Some(Value::Io(h)), None) = (args.next(), args.next()) { + let mut io = vm.get_io(*h).get_io(); + io.flush()?; + Ok(Value::True) + } else { + Err(VMError::new("io", "fflush takes one form (file)")) + } +} + +/// Usage: (fs-rm \"/dir/or/file/to/remove\") +/// +/// Takes a file or directory as a string and removes it. Works recursively for directories. +/// +/// Section: file +/// +/// Example: +/// (def fp nil) +/// (let (a-file (get-temp-file)) +/// (test::assert-true (fs-exists? a-file)) +/// (set! fp a-file) +/// (fs-rm a-file)) +/// (test::assert-false (nil? fp)) +/// (test::assert-false (fs-exists? fp)) +#[sl_sh_fn(fn_name = "fs-rm")] +fn fs_rm(path: String) -> VMResult { + let path = expand_tilde(path.into()); + let p = path.as_path(); + if p.exists() { + if p.is_dir() { + fs::remove_dir_all(p).map_err(|e| VMError::new("io", e.to_string()))?; + } else { + fs::remove_file(p).map_err(|e| VMError::new("io", e.to_string()))?; + }; + Ok(Value::True) } else { Err(VMError::new( "io", - "fs-exists?: takes a filename as only arg".to_string(), + format!("path does not exist: {}", path.display()), )) } } pub fn add_io_builtins(env: &mut SloshVm) { + intern_fs_rm(env); + add_builtin( env, - "fs-meta", - fs_meta, - r#"Usage: (fs-meta [FILENAME]) -> map + "fopen", + fopen, + "Usage: (fopen filename option*) -Returns a map of a files meta data. +Open a file. -Section: io -"#, +Options are: + :read + :write + :append + :truncate + :create + :create-new + :on-error-nil + +Section: file + +Example: +(def tmp (get-temp)) +(def test-open-f (fopen (str tmp \"/slsh-tst-open.txt\") :create :truncate)) +(fprn test-open-f \"Test Line One\") +(fclose test-open-f) +(test::assert-equal \"Test Line One\n\" (read-line (fopen (str tmp \"/slsh-tst-open.txt\")))) +", ); + add_builtin( + env, + "fclose", + fclose, + "Usage: (fclose file) + +Close a file. +Section: file + +Example: +(def tmp (get-temp)) +(def tst-file (fopen (str tmp \"/slsh-tst-open.txt\") :create :truncate)) +(fprn tst-file \"Test Line Two\") +(fclose tst-file) +(def tst-file (fopen (str tmp \"/slsh-tst-open.txt\") :read)) +(test::assert-equal \"Test Line Two\n\" (read-line tst-file)) +(fclose tst-file) +", + ); add_builtin( env, - "fs-exists?", - fs_exists, - r#"Usage: (fs-exists? path-to-test) + "read-line", + builtin_read_line, + r#"Usage: (read-line file) -> string -Does the given path exist? +Read a line from a file. Section: file Example: -($sh "rm" "-rf" "/tmp/tst-fs-exists") -($sh "mkdir" "/tmp/tst-fs-exists") -(test::assert-true (fs-exists? "/tmp/tst-fs-exists")) -(test::assert-false (fs-exists? "/tmp/tst-fs-exists/fs-exists-nope")) -($sh "rmdir" "/tmp/tst-fs-exists") +(with-temp-file (fn (tmp) + (let (tst-file (fopen tmp :create :truncate)) + (fprn tst-file "Test Line Read Line One") + (fpr tst-file "Test Line Read Line Two") + (fclose tst-file) + (set! tst-file (fopen tmp :read)) + (defer (fclose tst-file)) + (test::assert-equal "Test Line Read Line One\n" (read-line tst-file)) + (test::assert-equal "Test Line Read Line Two" (read-line tst-file))))) "#, ); + add_builtin( + env, + "fflush", + builtin_flush, + "Usage: (flush file) + +Flush a file. + +Section: file + +Example: +(def tmp (get-temp)) +(def tst-file (fopen (str tmp \"/slsh-tst-open.txt\") :create :truncate)) +(fprn tst-file \"Test Line Three\") +(fflush tst-file) +(def tst-file (fopen (str tmp \"/slsh-tst-open.txt\") :read)) +(test::assert-equal \"Test Line Three\n\" (read-line tst-file)) +(fclose tst-file) +", + ); } diff --git a/builtins/src/lib.rs b/builtins/src/lib.rs index 88acbed549..d599dee686 100644 --- a/builtins/src/lib.rs +++ b/builtins/src/lib.rs @@ -6,8 +6,11 @@ use slvm::{VMError, VMResult, Value}; pub mod bridge_macro_tests; pub mod collections; pub mod conversions; +pub mod fs_meta; +pub mod fs_temp; pub mod io; pub mod print; +pub mod rand; pub mod string; fn get_globals(vm: &mut SloshVm, registers: &[Value]) -> VMResult { diff --git a/builtins/src/print.rs b/builtins/src/print.rs index c7e8bc0c4e..f627b3f209 100644 --- a/builtins/src/print.rs +++ b/builtins/src/print.rs @@ -115,6 +115,39 @@ pub fn prn(vm: &mut SloshVm, registers: &[Value]) -> VMResult { Ok(Value::Nil) } +pub fn fpr(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let Some(Value::Io(h)) = args.next() { + let mut file = vm.get_io(*h).get_io(); + for v in args { + write!(file, "{}", pretty_value(vm, *v))?; + } + Ok(Value::Nil) + } else { + Err(VMError::new( + "io", + "fpr: require a writable IO object as first parameter", + )) + } +} + +pub fn fprn(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let Some(Value::Io(h)) = args.next() { + let mut file = vm.get_io(*h).get_io(); + for v in args { + write!(file, "{}", pretty_value(vm, *v))?; + } + writeln!(file)?; + Ok(Value::Nil) + } else { + Err(VMError::new( + "io", + "fpr: require a writable IO object as first parameter", + )) + } +} + pub fn dasm(vm: &mut SloshVm, registers: &[Value]) -> VMResult { if registers.len() != 1 { return Err(VMError::new_compile( @@ -140,4 +173,6 @@ pub fn add_print_builtins(env: &mut SloshVm) { env.set_global_builtin("pr", pr); env.set_global_builtin("prn", prn); env.set_global_builtin("dasm", dasm); + env.set_global_builtin("fpr", fpr); + env.set_global_builtin("fprn", fprn); } diff --git a/builtins/src/rand.rs b/builtins/src/rand.rs new file mode 100644 index 0000000000..57191158b9 --- /dev/null +++ b/builtins/src/rand.rs @@ -0,0 +1,273 @@ +use rand::distributions::{Alphanumeric, Distribution}; +use rand::Rng; +use std::iter; +use unicode_segmentation::UnicodeSegmentation; + +use bridge_adapters::add_builtin; +use compile_state::state::SloshVm; +use rand::rngs::ThreadRng; +use slvm::{from_i56, VMError, VMResult, Value}; +use std::borrow::Cow; + +fn builtin_random(_vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut rng = rand::thread_rng(); + let mut args = registers.iter(); + if let (Some(next_arg), None) = (args.next(), args.next()) { + match next_arg { + Value::Int(i) => { + let i: i64 = from_i56(i); + match i { + positive if positive > 0 => Ok(rng.gen_range(0..i).into()), + _ => Err(VMError::new("rand", "Expected positive number")), + } + } + Value::Float(f) => { + let f: f64 = (*f).into(); + match f { + positive if positive > 0.0 => Ok(rng.gen_range(0.0..f).into()), + _ => Err(VMError::new("rand", "Expected positive number")), + } + } + _ => Err(VMError::new( + "rand", + "Expected positive number, float or int", + )), + } + } else { + Err(VMError::new( + "rand", + "Expected positive number, float or int", + )) + } +} + +fn builtin_get_random_str(vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let mut args = registers.iter(); + if let (Some(Value::Int(i)), Some(arg2)) = (args.next(), args.next()) { + let i: i64 = from_i56(i); + match i { + positive if positive > 0 => get_random_str(vm, *arg2, positive as u64), + _ => Err(VMError::new("rand", "Expected positive number")), + } + } else { + Err(VMError::new("rand", "Expected at least one number")) + } +} + +pub fn rand_alphanumeric_str(len: u64, rng: &mut ThreadRng) -> Cow<'static, str> { + iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(len as usize) + .collect() +} + +fn get_random_str(vm: &mut SloshVm, arg: Value, len: u64) -> VMResult { + let mut rng = rand::thread_rng(); + match arg { + Value::Symbol(i) => { + let sym = vm.get_interned(i); + match sym { + ":ascii" => Ok(vm.alloc_string( + iter::repeat(()) + .map(|()| rng.sample(Ascii)) + .map(char::from) + .take(len as usize) + .collect(), + )), + ":alnum" => Ok(vm.alloc_string(rand_alphanumeric_str(len, &mut rng).to_string())), + ":hex" => Ok(vm.alloc_string( + iter::repeat(()) + .map(|()| rng.sample(Hex)) + .map(char::from) + .take(len as usize) + .collect(), + )), + _ => Err(VMError::new("rand", format!("Unknown symbol {}", sym))), + } + } + Value::String(h) => { + let string = vm.get_string(h); + let upg = UserProvidedGraphemes::new(string); + Ok(vm.alloc_string( + iter::repeat(()) + .map(|()| rng.sample(&upg)) + .take(len as usize) + .collect(), + )) + } + _ => Err(VMError::new( + "rand", + "Second argument must be keyword or string", + )), + } +} + +#[derive(Debug)] +struct Ascii; + +impl Distribution for Ascii { + fn sample(&self, rng: &mut R) -> u8 { + const RANGE: u32 = 26 + 26 + 10 + 32; + const ASCII_PRINTABLE_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789\ + !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; + // We can pick from 94 characters. This is so close to a power of 2, 128, + // that we can do better than `Uniform`. Use a simple bitshift and + // rejection sampling. We do not use a bitmask, because for small RNGs + // the most significant bits are usually of higher quality. + loop { + let var = rng.next_u32() >> (32 - 7); + if var < RANGE { + return ASCII_PRINTABLE_CHARSET[var as usize]; + } + } + } +} + +#[derive(Debug)] +struct Hex; + +impl Distribution for Hex { + fn sample(&self, rng: &mut R) -> u8 { + const RANGE: u32 = 16; + const HEX_CHARSET: &[u8] = b"abcdef0123456789"; + // We can pick from 16 characters. This is a power of 2. Use a + // simple bitshift and rejection sampling. We do not use a bitmask, + // because for small RNGs/ the most significant bits are usually + // of higher quality. + loop { + let var = rng.next_u32() >> (32 - 4); + if var < RANGE { + return HEX_CHARSET[var as usize]; + } + } + } +} + +#[derive(Debug)] +struct UserProvidedGraphemes { + sample_space: Vec, + len: usize, +} + +impl UserProvidedGraphemes { + pub fn new(s: &str) -> UserProvidedGraphemes { + let mut sample_space: Vec = Vec::new(); + for cluster in UnicodeSegmentation::graphemes(s, true) { + sample_space.push(cluster.to_string()); + } + let len = sample_space.len(); + UserProvidedGraphemes { sample_space, len } + } +} + +impl Distribution for UserProvidedGraphemes { + fn sample(&self, rng: &mut R) -> String { + self.sample_space + .get(rng.gen_range(0..self.len)) + .unwrap() + .to_owned() + } +} + +fn builtin_probool(_vm: &mut SloshVm, registers: &[Value]) -> VMResult { + let tup: Option<(u32, u32)> = match (registers.first(), registers.get(1), registers.get(2)) { + (None, None, None) => Some((1, 2)), + (Some(Value::Int(first)), Some(Value::Int(second)), None) => { + let i: i64 = from_i56(first); + let j: i64 = from_i56(second); + if i > 0 && i < u32::MAX as i64 && j > 0 && j < u32::MAX as i64 { + Some((i as u32, j as u32)) + } else { + None + } + } + _ => None, + }; + match tup { + None => Err(VMError::new("rand", "Expected zero or two positive ints")), + Some((_, 0)) => Err(VMError::new("rand", "Denominator can not be zero")), + Some((i, j)) => match i / j { + improper if improper > 1 => Ok(Value::True), + _ => match rand::thread_rng().gen_ratio(i, j) { + true => Ok(Value::True), + false => Ok(Value::False), + }, + }, + } +} + +pub fn add_rand_builtins(env: &mut SloshVm) { + add_builtin( + env, + "probool", + builtin_probool, + "Usage: (probool), (probool numerator denominator) + +PRObability of a BOOLean. + +If no arguments are given, returns #t 1/2 of the time, otherwise takes two +integers, numerator and denominator, and returns #t numerator/denominator of the +time. Throws an error if denominator is 0. If (>= (/ numerator denominator) 1) +probool always returns true. If numerator is 0 probool always returns false. + +Section: random + +Example: +(def val0 (probool)) +(test::assert-true (or (= #t val0) (= nil val0))) +(def val1 (probool 17 42)) +(test::assert-true (or (= #t val1) (= nil val1))) +(test::assert-true (probool 1 1)) +(test::assert-false (probool 0 42)) +(test::assert-error-msg (probool 0 0) \"Denominator can not be zero\") +(test::assert-error-msg (probool 0 0 0) \"Expected zero or two numbers\") +", + ); + + add_builtin(env, + "random-str", + builtin_get_random_str, + "Usage: (random-str str-length [char-set]) + +Takes a positive integer, str-length, and one of :hex, :ascii, :alnum, or +a string. Returns random string of provided str-length composed of second argument, +:hex results in random hex string, :ascii results in random string of all printable +ascii characters, :alnum results in random string of all alphanumeric characters, +and providing a string results in a random string composed by sampling input. + +Section: random + +Example: +(test::assert-error-msg (random-str) \"random-str: Missing required argument, see (doc 'random-str) for usage.\") +(test::assert-error-msg (random-str -1) \"Expected positive number\") +(test::assert-error-msg (random-str 10) \"random-str: Missing required argument, see (doc 'random-str) for usage.\") +(test::assert-equal 100 (length (random-str 10 :hex)) +(test::assert-true (str-contains \"\u{2699}\" (random-str 42 \"\u{2699}\")) +(test::assert-equal 19 (length (random-str 19 :ascii) +(test::assert-equal 91 (length (random-str 91 :alnum) +", + ); + + add_builtin( + env, + "random", + builtin_random, + "Usage: (random), (random limit) + +Returns non-negative number less than limit and of the same type as limit. + +Section: random + +Example: +(def rand-int (random 100)) +(test::assert-true (and (> rand-int 0) (< rand-int 100)) +(def rand-float (random 1.0)) +(test::assert-true (and (> rand-float 0) (< rand-float 1))) +(test::assert-error-msg (random -1) \"Expected positive integer\") +(test::assert-error-msg (random 1 2) \"Expected zero or one integers\") +", + ); +} diff --git a/compiler/src/compile/compile_let.rs b/compiler/src/compile/compile_let.rs index 81d9798d1c..f162e8a232 100644 --- a/compiler/src/compile/compile_let.rs +++ b/compiler/src/compile/compile_let.rs @@ -297,9 +297,6 @@ fn let_while_inner( } let mut free_reg2 = result; compile_right_exps(env, right_exps, state, symbols.clone(), &mut free_reg2)?; - /*if free_reg2 != result && free_reg != free_reg2 { - panic!("mismatch in let-while lets!"); - }XXXX*/ state .chunk diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs index d1620dbb98..fc925af2eb 100644 --- a/compiler/src/lib.rs +++ b/compiler/src/lib.rs @@ -5,6 +5,8 @@ pub use crate::reader::*; use builtins::add_misc_builtins; use builtins::collections::setup_collection_builtins; use builtins::conversions::add_conv_builtins; +use builtins::fs_meta::add_fs_meta_builtins; +use builtins::fs_temp::add_fs_temp_builtins; use builtins::io::add_io_builtins; use builtins::print::add_print_builtins; use builtins::string::add_str_builtins; @@ -34,6 +36,8 @@ pub fn set_builtins(env: &mut SloshVm) { add_str_builtins(env); add_misc_builtins(env); add_io_builtins(env); + add_fs_meta_builtins(env); + add_fs_temp_builtins(env); add_conv_builtins(env); env.set_named_global("*int-bits*", (INT_BITS as i64).into()); diff --git a/compiler/src/load_eval.rs b/compiler/src/load_eval.rs index d51146ea2f..012a794386 100644 --- a/compiler/src/load_eval.rs +++ b/compiler/src/load_eval.rs @@ -250,7 +250,11 @@ fn eval_exp(vm: &mut SloshVm, exp: Value) -> VMResult { compile(vm, &mut state, exp, 0)?; state.chunk.encode0(RET, vm.own_line())?; let chunk = Arc::new(state.chunk.clone()); - vm.do_call(chunk, &[], None) + let l = vm.alloc_lambda(chunk.clone()); + vm.heap_sticky(l); + let ret = vm.do_call(chunk, &[], None); + vm.heap_unsticky(l); + ret } /// Builtin eval implementation. Tries to avoid compilation when possible (uses apply machinery). @@ -283,11 +287,11 @@ fn contains_list(args: &[Value]) -> bool { false } -/// Internal implementation, args is expected to have at least one value (the callable being called). +/// Call lambda with args, this is re-entrant. fn apply_callable(vm: &mut SloshVm, lambda: Value, args: &[Value]) -> VMResult { match lambda { Value::Symbol(i) | Value::Special(i) if i == vm.specials().quote => { - if let Some(arg) = args.get(1) { + if let Some(arg) = args.first() { Ok(*arg) } else { Err(VMError::new_vm( @@ -309,7 +313,7 @@ fn apply_callable(vm: &mut SloshVm, lambda: Value, args: &[Value]) -> VMResult { let mut args_t; - let args = if contains_list(args) { + let mut args = if contains_list(args) { args_t = args.to_vec(); for i in args_t.iter_mut() { // quote any lists so they do not get compiled... @@ -319,6 +323,7 @@ fn apply_callable(vm: &mut SloshVm, lambda: Value, args: &[Value]) -> VMResult VMResult { let b = vm.get_builtin(i); - (b)(vm, &args[1..]) + (b)(vm, args) } Value::Lambda(h) => { let l = vm.get_lambda(h); - vm.do_call(l, &args[1..], None) + vm.do_call(l, args, None) } Value::Closure(h) => { let (l, caps) = vm.get_closure(h); let caps = caps.to_vec(); - vm.do_call(l, &args[1..], Some(&caps[..])) + vm.do_call(l, args, Some(&caps[..])) } Value::Continuation(_handle) => { // It probably does not make sense to use apply with a continuation, it can only take @@ -346,7 +351,9 @@ fn apply_callable(vm: &mut SloshVm, lambda: Value, args: &[Value]) -> VMResult VMResult { } else { registers.to_vec() }; - apply_callable(vm, v[0], &v[..]) + apply_callable(vm, v[0], &v[1..]) } fn apply(vm: &mut SloshVm, registers: &[Value]) -> VMResult { diff --git a/lisp/core.slosh b/lisp/core.slosh index 57c32cdeae..fc9c3eb233 100644 --- a/lisp/core.slosh +++ b/lisp/core.slosh @@ -658,3 +658,97 @@ Asserts the value is an error Section: core %# (defmacro test::assert-error (& body) `(assert-error ~@body)) + + +#% +Usage: (with-temp-file (fn (x) (println "given temp file:" x)) ["optional-prefix" "optional-suffix" length]) + +Takes a function that accepts a temporary file. This file will be removed when the provided function +is finished executing. Also accepts an optional prefix, an optional suffix, and an optional +length for the random number of characters in the temporary file created. Defaults to prefix of +".tmp", no suffix, and five random characters. + +Section: file + +Example: +(def fp nil) +(with-temp-file (fn (tmp-file) + (let (a-file (fopen tmp-file :create :truncate)) + (test::assert-true (fs-exists? tmp-file)) + (set! fp tmp-file) + (fclose a-file)))) +(test::assert-false (nil? fp)) +(test::assert-false (fs-exists? fp)) + +(with-temp-file + (fn (tmp) + (test::assert-true (str-contains tmp "some-prefix"))) + "some-prefix") + +(with-temp-file + (fn (tmp) + (test::assert-true (str-contains tmp "some-prefix")) + (test::assert-true (str-contains tmp "some-suffix"))) + "some-prefix" + "some-suffix") + +(with-temp-file + (fn (tmp) + (test::assert-true (str-contains tmp "some-prefix")) + (test::assert-true (str-contains tmp "some-suffix")) + (test::assert-equal (len "some-prefix0123456789some-suffix") (len (fs-base tmp)))) + "some-prefix" + "some-suffix" + 10) +%# +(defn with-temp-file (func & args) + (let (file-name (apply get-temp-file (temp-dir) args)) + (defer (fs-rm file-name)) + (func file-name))) + +#% +Usage: (with-temp (fn (x) (println "given temp dir:" x)) ["optional-prefix" "optional-suffix" length]) + +Takes a function that accepts a temporary directory. This directory will be recursively removed +when the provided function is finished executing. Also accepts an optional prefix, an optional +suffix, and an optional length for the random number of characters in the temporary directory +created. Defaults to prefix of \".tmp\", no suffix, and five random characters. + +Section: file + +Example: +(def fp nil) +(with-temp (fn (tmp-dir) + (let (tmp-file (str tmp-dir "/sl-sh-tmp-file.txt") + a-file (fopen tmp-file :create :truncate)) + (test::assert-true (fs-exists? tmp-file)) + (set! fp tmp-file) + (fclose a-file)))) +(test::assert-false (nil? fp)) +(test::assert-false (fs-exists? fp)) + +(with-temp + (fn (tmp) + (test::assert-true (str-contains tmp "some-prefix"))) + "some-prefix") + +(with-temp + (fn (tmp) + (test::assert-true (str-contains tmp "some-prefix")) + (test::assert-true (str-contains tmp "some-suffix"))) + "some-prefix" + "some-suffix") + +(with-temp + (fn (tmp) + (test::assert-true (str-contains tmp "some-prefix")) + (test::assert-true (str-contains tmp "some-suffix")) + (test::assert-equal (len "some-prefix0123456789some-suffix") (len (fs-base tmp)))) + "some-prefix" + "some-suffix" + 10) +%# +(defn with-temp (func & args) + (let (dir (apply get-temp (temp-dir) args)) + (defer (fs-rm dir)) + (func dir))) diff --git a/slosh_lib/src/shell_builtins.rs b/slosh_lib/src/shell_builtins.rs index 3d1f5cd49c..349a24b452 100644 --- a/slosh_lib/src/shell_builtins.rs +++ b/slosh_lib/src/shell_builtins.rs @@ -131,5 +131,5 @@ Section: core"#, sh, "Runs a shell command and returns it's status.", ); - add_builtin(env, "env", env_var, "Retrieves and environment variable."); + add_builtin(env, "env", env_var, "Retrieves an environment variable."); } diff --git a/slosh_test/Cargo.toml b/slosh_test/Cargo.toml index cc5d9c3e58..be285db934 100644 --- a/slosh_test/Cargo.toml +++ b/slosh_test/Cargo.toml @@ -12,6 +12,7 @@ bridge_adapters = { path = "../bridge_adapters" } slvm = { workspace = true } sl-compiler = { workspace = true } compile_state = { workspace = true } +shell = { workspace = true } mdbook = "0.4" [dev-dependencies] diff --git a/slosh_test/src/docs.rs b/slosh_test/src/docs.rs index ef7117375c..af9471dd62 100644 --- a/slosh_test/src/docs.rs +++ b/slosh_test/src/docs.rs @@ -68,6 +68,8 @@ lazy_static! { exemption_set.insert("*int-max*"); exemption_set.insert("prn"); exemption_set.insert("pr"); + exemption_set.insert("fprn"); + exemption_set.insert("fpr"); exemption_set.insert("sizeof-value"); exemption_set.insert("dump-regs"); exemption_set.insert("dasm"); @@ -474,7 +476,8 @@ impl SlFrom for Value { fn doc_map(vm: &mut SloshVm, registers: &[Value]) -> VMResult { let mut i = registers.iter(); - match (i.next(), i.next()) { + vm.pause_gc(); + let res = match (i.next(), i.next()) { (Some(Value::Symbol(g)), None) => match SloshDoc::new(*g, vm, Namespace::Global) { Ok(slosh_doc) => Value::sl_from(slosh_doc, vm), Err(DocError::ExemptFromProperDocString { symbol: _ }) => { @@ -484,7 +487,9 @@ fn doc_map(vm: &mut SloshVm, registers: &[Value]) -> VMResult { Err(e) => Err(VMError::from(e)), }, _ => Err(VMError::new_vm("takes one argument (symbol)".to_string())), - } + }; + vm.unpause_gc(); + res } /// Each doc has a tag in its `Section:` definition by convention that logically groups functions. @@ -754,6 +759,8 @@ mod test { let home_dir = tmp_dir.path().to_str(); let home_path = home_dir.unwrap().to_string(); + // Need to mask signals in case any tests shell out (use 'sh' or '$sh') otherwise test will hang but appear to finish... + shell::signals::mask_signals(); temp_env::with_var("HOME", home_dir, || { ENV.with(|env| { let mut vm = env.borrow_mut(); diff --git a/slosh_test/tests/slosh-tests.rs b/slosh_test/tests/slosh-tests.rs index 989d8cb212..0083d5c893 100644 --- a/slosh_test/tests/slosh-tests.rs +++ b/slosh_test/tests/slosh-tests.rs @@ -21,7 +21,7 @@ pub fn get_slosh_exe() -> PathBuf { /// fn run_slosh_tests() { let test_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("run-tests.slosh"); - println!("Slosh doc test script: {test_script:?}"); + eprintln!("Slosh doc test script: {test_script:?}"); let slosh_path = get_slosh_exe().into_os_string(); let slosh_path = slosh_path.to_str(); diff --git a/vm/src/heap/io.rs b/vm/src/heap/io.rs index ebd5eadb62..9fed798b19 100644 --- a/vm/src/heap/io.rs +++ b/vm/src/heap/io.rs @@ -3,6 +3,7 @@ use std::io; use std::io::{BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write}; use std::sync::{Arc, Mutex, MutexGuard}; +#[derive(Copy, Clone, Debug)] pub enum HeapIoError { Closed, NotFile, @@ -19,6 +20,21 @@ impl HeapIo { Self { io } } + pub fn stdin() -> Self { + let io = Arc::new(Mutex::new(Io::StdIn)); + Self { io } + } + + pub fn stdout() -> Self { + let io = Arc::new(Mutex::new(Io::StdOut)); + Self { io } + } + + pub fn stderr() -> Self { + let io = Arc::new(Mutex::new(Io::StdErr)); + Self { io } + } + pub fn close(&self) { if let Ok(mut guard) = self.io.lock() { *guard = Io::Closed @@ -34,6 +50,9 @@ impl HeapIo { }, Io::FileReadBuf(_) => return Err(HeapIoError::NotFile), Io::FileWriteBuf(_) => return Err(HeapIoError::NotFile), + Io::StdIn => return Err(HeapIoError::NotFile), + Io::StdOut => return Err(HeapIoError::NotFile), + Io::StdErr => return Err(HeapIoError::NotFile), Io::Closed => return Err(HeapIoError::Closed), } } @@ -49,6 +68,9 @@ impl HeapIo { }, Io::FileReadBuf(_) => return Err(HeapIoError::NotFile), Io::FileWriteBuf(_) => return Err(HeapIoError::NotFile), + Io::StdIn => return Err(HeapIoError::NotFile), + Io::StdOut => return Err(HeapIoError::NotFile), + Io::StdErr => return Err(HeapIoError::NotFile), Io::Closed => return Err(HeapIoError::Closed), } } @@ -91,6 +113,9 @@ enum Io { File(Option), FileReadBuf(BufReader), FileWriteBuf(BufWriter), + StdIn, + StdOut, + StdErr, Closed, } @@ -104,6 +129,15 @@ impl Read for Io { ErrorKind::Unsupported, "read not supported for a write buffer", )), + Io::StdIn => io::stdin().read(buf), + Io::StdOut => Err(io::Error::new( + ErrorKind::Unsupported, + "read not supported for stdout", + )), + Io::StdErr => Err(io::Error::new( + ErrorKind::Unsupported, + "read not supported for stderr", + )), Io::Closed => Err(io::Error::new( ErrorKind::Unsupported, "read not supported for closed", @@ -122,6 +156,12 @@ impl Write for Io { "write not supported for a read buffer", )), Io::FileWriteBuf(io) => io.write(buf), + Io::StdIn => Err(io::Error::new( + ErrorKind::Unsupported, + "write not supported for stdin", + )), + Io::StdOut => io::stdout().write(buf), + Io::StdErr => io::stderr().write(buf), Io::Closed => Err(io::Error::new( ErrorKind::Unsupported, "write not supported for closed", @@ -138,6 +178,12 @@ impl Write for Io { "flush not supported for a read buffer", )), Io::FileWriteBuf(io) => io.flush(), + Io::StdIn => Err(io::Error::new( + ErrorKind::Unsupported, + "flush not supported for stdin", + )), + Io::StdOut => io::stdout().flush(), + Io::StdErr => io::stderr().flush(), Io::Closed => Err(io::Error::new( ErrorKind::Unsupported, "flush not supported for closed", @@ -153,6 +199,18 @@ impl Seek for Io { Io::File(None) => panic!("file is missing a file"), Io::FileReadBuf(io) => io.seek(pos), Io::FileWriteBuf(io) => io.seek(pos), + Io::StdIn => Err(io::Error::new( + ErrorKind::Unsupported, + "seek not supported for stdin", + )), + Io::StdOut => Err(io::Error::new( + ErrorKind::Unsupported, + "seek not supported for stdout", + )), + Io::StdErr => Err(io::Error::new( + ErrorKind::Unsupported, + "seek not supported for stderr", + )), Io::Closed => Err(io::Error::new( ErrorKind::Unsupported, "seek not supported for closed", diff --git a/vm/src/vm/storage.rs b/vm/src/vm/storage.rs index b2351d3887..98c639e8e9 100644 --- a/vm/src/vm/storage.rs +++ b/vm/src/vm/storage.rs @@ -240,6 +240,15 @@ impl GVm { res } + /// Allocate a Value on the heap. Moving a value to the heap is useful for captured variable + /// for instance. + pub fn alloc_io(&mut self, io: HeapIo) -> Value { + let mut heap = self.heap.take().expect("VM must have a Heap!"); + let res = heap.alloc_io(io, MutState::Mutable, |heap| self.mark_roots(heap)); + self.heap = Some(heap); + res + } + pub fn heap_immutable(&mut self, val: Value) { self.heap_mut().immutable(val); } From 9854cda8e616d487cf86dd92c8e849a7dbafcc8b Mon Sep 17 00:00:00 2001 From: Steven Stanfield Date: Wed, 1 May 2024 00:43:42 -0400 Subject: [PATCH 2/4] fmt --- slosh_lib/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/slosh_lib/src/lib.rs b/slosh_lib/src/lib.rs index a8e927a241..cebba7cc66 100644 --- a/slosh_lib/src/lib.rs +++ b/slosh_lib/src/lib.rs @@ -19,15 +19,15 @@ use sl_compiler::reader::*; use builtins::collections::setup_collection_builtins; use builtins::conversions::add_conv_builtins; +use builtins::fs_meta::add_fs_meta_builtins; +use builtins::fs_temp::add_fs_temp_builtins; use builtins::io::add_io_builtins; use builtins::print::{add_print_builtins, display_value}; +use builtins::rand::add_rand_builtins; use builtins::string::add_str_builtins; use builtins::{add_global_value, add_misc_builtins}; use sl_liner::vi::AlphanumericAndVariableKeywordRule; use sl_liner::{keymap, ColorClosure, Context, Prompt}; -use builtins::fs_meta::add_fs_meta_builtins; -use builtins::fs_temp::add_fs_temp_builtins; -use builtins::rand::add_rand_builtins; mod completions; pub mod debug; From ada142a1a752c22ccc7a1367a4fd15117c5d0d1c Mon Sep 17 00:00:00 2001 From: Steven Stanfield Date: Thu, 2 May 2024 00:37:36 -0400 Subject: [PATCH 3/4] Hooked up and fixed the random builtins. --- builtins/src/rand.rs | 58 ++++++++++++++++++++++++++++---------------- lisp/core.slosh | 24 ++++++++++++++++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/builtins/src/rand.rs b/builtins/src/rand.rs index 57191158b9..b506d249f5 100644 --- a/builtins/src/rand.rs +++ b/builtins/src/rand.rs @@ -47,10 +47,13 @@ fn builtin_get_random_str(vm: &mut SloshVm, registers: &[Value]) -> VMResult 0 => get_random_str(vm, *arg2, positive as u64), - _ => Err(VMError::new("rand", "Expected positive number")), + _ => Err(VMError::new("rand", "Expected positive length")), } } else { - Err(VMError::new("rand", "Expected at least one number")) + Err(VMError::new( + "rand", + "Expected two arguments, length and charset", + )) } } @@ -65,25 +68,25 @@ pub fn rand_alphanumeric_str(len: u64, rng: &mut ThreadRng) -> Cow<'static, str> fn get_random_str(vm: &mut SloshVm, arg: Value, len: u64) -> VMResult { let mut rng = rand::thread_rng(); match arg { - Value::Symbol(i) => { + Value::Keyword(i) => { let sym = vm.get_interned(i); match sym { - ":ascii" => Ok(vm.alloc_string( + "ascii" => Ok(vm.alloc_string( iter::repeat(()) .map(|()| rng.sample(Ascii)) .map(char::from) .take(len as usize) .collect(), )), - ":alnum" => Ok(vm.alloc_string(rand_alphanumeric_str(len, &mut rng).to_string())), - ":hex" => Ok(vm.alloc_string( + "alnum" => Ok(vm.alloc_string(rand_alphanumeric_str(len, &mut rng).to_string())), + "hex" => Ok(vm.alloc_string( iter::repeat(()) .map(|()| rng.sample(Hex)) .map(char::from) .take(len as usize) .collect(), )), - _ => Err(VMError::new("rand", format!("Unknown symbol {}", sym))), + _ => Err(VMError::new("rand", format!("Unknown symbol :{}", sym))), } } Value::String(h) => { @@ -96,6 +99,16 @@ fn get_random_str(vm: &mut SloshVm, arg: Value, len: u64) -> VMResult { .collect(), )) } + Value::StringConst(i) => { + let string = vm.get_interned(i); + let upg = UserProvidedGraphemes::new(string); + Ok(vm.alloc_string( + iter::repeat(()) + .map(|()| rng.sample(&upg)) + .take(len as usize) + .collect(), + )) + } _ => Err(VMError::new( "rand", "Second argument must be keyword or string", @@ -178,7 +191,7 @@ fn builtin_probool(_vm: &mut SloshVm, registers: &[Value]) -> VMResult { (Some(Value::Int(first)), Some(Value::Int(second)), None) => { let i: i64 = from_i56(first); let j: i64 = from_i56(second); - if i > 0 && i < u32::MAX as i64 && j > 0 && j < u32::MAX as i64 { + if i >= 0 && i < u32::MAX as i64 && j >= 0 && j < u32::MAX as i64 { Some((i as u32, j as u32)) } else { None @@ -222,12 +235,13 @@ Example: (test::assert-true (or (= #t val1) (= nil val1))) (test::assert-true (probool 1 1)) (test::assert-false (probool 0 42)) -(test::assert-error-msg (probool 0 0) \"Denominator can not be zero\") -(test::assert-error-msg (probool 0 0 0) \"Expected zero or two numbers\") +(test::assert-error-msg (probool 0 0) :rand \"Denominator can not be zero\") +(test::assert-error-msg (probool 0 0 0) :rand \"Expected zero or two positive ints\") ", ); - add_builtin(env, + add_builtin( + env, "random-str", builtin_get_random_str, "Usage: (random-str str-length [char-set]) @@ -241,13 +255,15 @@ and providing a string results in a random string composed by sampling input. Section: random Example: -(test::assert-error-msg (random-str) \"random-str: Missing required argument, see (doc 'random-str) for usage.\") -(test::assert-error-msg (random-str -1) \"Expected positive number\") -(test::assert-error-msg (random-str 10) \"random-str: Missing required argument, see (doc 'random-str) for usage.\") -(test::assert-equal 100 (length (random-str 10 :hex)) -(test::assert-true (str-contains \"\u{2699}\" (random-str 42 \"\u{2699}\")) -(test::assert-equal 19 (length (random-str 19 :ascii) -(test::assert-equal 91 (length (random-str 91 :alnum) +(test::assert-error-msg (random-str) :rand \"Expected two arguments, length and charset\") +(test::assert-error-msg (random-str 10) :rand \"Expected two arguments, length and charset\") +(test::assert-error-msg (random-str -1 :hex) :rand \"Expected positive length\") +(test::assert-error-msg (random-str 10 1) :rand \"Second argument must be keyword or string\") +(test::assert-error-msg (random-str 1 :hexy) :rand \"Unknown symbol :hexy\") +(test::assert-equal 10 (len (random-str 10 :hex))) +(test::assert-true (str-contains (random-str 42 \"\u{2699}\") \"\u{2699}\")) +(test::assert-equal 19 (len (random-str 19 :ascii))) +(test::assert-equal 91 (len (random-str 91 :alnum))) ", ); @@ -263,11 +279,11 @@ Section: random Example: (def rand-int (random 100)) -(test::assert-true (and (> rand-int 0) (< rand-int 100)) +(test::assert-true (and (> rand-int 0) (< rand-int 100))) (def rand-float (random 1.0)) (test::assert-true (and (> rand-float 0) (< rand-float 1))) -(test::assert-error-msg (random -1) \"Expected positive integer\") -(test::assert-error-msg (random 1 2) \"Expected zero or one integers\") +(test::assert-error-msg (random -1) :rand \"Expected positive number\") +(test::assert-error-msg (random 1 2) :rand \"Expected positive number, float or int\") ", ); } diff --git a/lisp/core.slosh b/lisp/core.slosh index 1524e7ecc3..ea888f1f6c 100644 --- a/lisp/core.slosh +++ b/lisp/core.slosh @@ -677,6 +677,30 @@ Section: test %# (defmacro test::assert-error (& body) `(assert-error ~@body)) +#% +Test asserts an error is thrown with a given key and message. + +Section: test + +Example: +(test::assert-error-msg (err "error thrown") :error "error thrown") +%# +(defmacro assert-error-msg + (form key msg) + `((fn (ret) (test::assert-true (and (= ~key (car ret)) (= ~msg (cdr ret))) + (str ". Expected \n" ~key ", [" ~msg "]\n => Test returned:\n" (car ret) ", [" (cdr ret) "]"))) + (get-error ~form))) + +#% +Test asserts an error is thrown with a given key and message. + +Section: test + +Example: +(test::assert-error-msg (err \"error thrown\") :error \"error thrown\") +%# +(defmacro test::assert-error-msg (& body) `(assert-error-msg ~@body)) + #% Usage: (with-temp-file (fn (x) (println "given temp file:" x)) ["optional-prefix" "optional-suffix" length]) From 8969b11ec462700b1ecf226f0075525f5ed065f5 Mon Sep 17 00:00:00 2001 From: Steven Stanfield Date: Mon, 6 May 2024 14:55:46 -0400 Subject: [PATCH 4/4] Added fs-crawl back, exposed hashmap bug... --- builtins/src/fs_meta.rs | 107 +++++++++++++++++++++----------------- compiler/src/load_eval.rs | 2 +- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/builtins/src/fs_meta.rs b/builtins/src/fs_meta.rs index e62761944f..b28596041b 100644 --- a/builtins/src/fs_meta.rs +++ b/builtins/src/fs_meta.rs @@ -2,8 +2,8 @@ use bridge_macros::sl_sh_fn; use bridge_types::VarArgs; use compile_state::state::SloshVm; use shell::builtins::expand_tilde; -//use slvm::{from_i56, VMError, VMResult, Value}; -use slvm::{VMError, VMResult, Value}; +use sl_compiler::load_eval::apply_callable; +use slvm::{from_i56, VMError, VMResult, Value}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::{env, fs, io}; @@ -14,7 +14,7 @@ use bridge_adapters::add_builtin; use same_file; use std::fs::{File, Metadata}; use std::time::SystemTime; -//use walkdir::{DirEntry, WalkDir}; +use walkdir::{DirEntry, WalkDir}; fn cd_expand_all_dots(cd: String) -> String { let mut all_dots = false; @@ -327,8 +327,7 @@ fn is_same_file(path_0: &str, path_1: &str) -> VMResult { } } -/* -/// Usage: (fs-crawl /path/to/file/or/dir (fn (x) (println "found path" x) [max-depth] +/// Usage: (fs-crawl /path/to/file/or/dir (fn (x) (prn "found path" x) [max-depth] /// [:follow-syms]) /// /// If a directory is provided the path is recursively searched and every @@ -345,77 +344,81 @@ fn is_same_file(path_0: &str, path_1: &str) -> VMResult { /// Example: /// /// (with-temp-file (fn (tmp-file) -/// (def cnt 0) +/// (let (cnt 0) /// (fs-crawl tmp-file (fn (x) /// (test::assert-equal (fs-base tmp-file) (fs-base x)) /// (set! cnt (+ 1 cnt)))) -/// (test::assert-equal 1 cnt))) +/// (test::assert-equal 1 cnt)))) +/// /// /// (defn create-in (in-dir num-files visited) /// (dotimes-i i num-files -/// (hash-set! visited (get-temp-file in-dir) nil))) +/// (let (tmp-file (get-temp-file in-dir)) +/// (set! visited.~tmp-file nil)))) /// /// (defn create-dir (tmp-dir visited) -/// (let ((new-tmp (get-temp tmp-dir))) -/// (hash-set! visited new-tmp nil) +/// (let (new-tmp (get-temp tmp-dir)) +/// (set! visited.~new-tmp #f) /// new-tmp)) /// +/// #| XXXX Fix hashmaps so strings and string consts hash the same then restore this testing... /// (with-temp (fn (root-tmp-dir) -/// (let ((tmp-file-count 5) -/// (visited (make-hash))) +/// (let (tmp-file-count 5 +/// visited {}) /// (def cnt 0) -/// (hash-set! visited root-tmp-dir nil) +/// (set! visited.~root-tmp-dir nil) /// (create-in root-tmp-dir tmp-file-count visited) -/// (let* ((tmp-dir (create-dir root-tmp-dir visited)) -/// (new-files (create-in tmp-dir tmp-file-count visited)) -/// (tmp-dir (create-dir tmp-dir visited)) -/// (new-files (create-in tmp-dir tmp-file-count visited))) +/// (let (tmp-dir (create-dir root-tmp-dir visited) +/// new-files (create-in tmp-dir tmp-file-count visited) +/// tmp-dir (create-dir tmp-dir visited) +/// new-files (create-in tmp-dir tmp-file-count visited)) /// (fs-crawl root-tmp-dir (fn (x) -/// (let ((file (hash-get visited x))) +/// (let (file visited.~x) /// (test::assert-true (not file)) ;; also tests double counting -/// (hash-set! visited x #t) -/// (set! cnt (+ 1 cnt))))) +/// (set! visited.~x #t) +/// (inc! cnt)))) /// (test::assert-equal (+ 3 (* 3 tmp-file-count)) cnt) -/// (test::assert-equal (+ 3 (* 3 tmp-file-count)) (length (hash-keys visited))) -/// (iterator::map (fn (x) (test::assert-true (hash-get visited y))) (hash-keys visited)))))) +/// (test::assert-equal (+ 3 (* 3 tmp-file-count)) (len (hash-keys visited))) +/// (seq-for key in (hash-keys visited) (test::assert-true visited.~key)))))) /// /// (with-temp (fn (root-tmp-dir) -/// (let ((tmp-file-count 5) -/// (visited (make-hash))) +/// (let (tmp-file-count 5 +/// visited {}) /// (def cnt 0) -/// (hash-set! visited root-tmp-dir nil) +/// (set! visited.~root-tmp-dir nil) /// (create-in root-tmp-dir tmp-file-count visited) -/// (let* ((tmp-dir (create-dir root-tmp-dir visited)) -/// (new-files (create-in tmp-dir tmp-file-count visited)) -/// (tmp-dir (create-dir tmp-dir (make-hash))) -/// (new-files (create-in tmp-dir tmp-file-count (make-hash)))) +/// (let (tmp-dir (create-dir root-tmp-dir visited) +/// new-files (create-in tmp-dir tmp-file-count visited) +/// tmp-dir (create-dir tmp-dir {}) +/// new-files (create-in tmp-dir tmp-file-count {})) /// (fs-crawl root-tmp-dir (fn (x) -/// (let ((file (hash-get visited x))) +/// (let (file visited.~x) /// (test::assert-true (not file)) ;; also tests double counting -/// (hash-set! visited x #t) -/// (set! cnt (+ 1 cnt)))) 2) +/// (set! visited.~x #t) +/// (inc! cnt))) 2) /// (test::assert-equal (+ 3 (* 2 tmp-file-count)) cnt) /// (test::assert-equal (+ 3 (* 2 tmp-file-count)) (length (hash-keys visited))) -/// (iterator::map (fn (x) (test::assert-true (hash-get visited y))) (hash-keys visited)))))) +/// (seq-for key in (hash-keys visited) (test::assert-true visited.~key))))) /// /// (with-temp (fn (root-tmp-dir) -/// (let ((tmp-file-count 5) -/// (visited (make-hash))) +/// (let (tmp-file-count 5 +/// visited {}) /// (def cnt 0) -/// (hash-set! visited root-tmp-dir nil) +/// (set! visited.~root-tmp-dir nil) /// (create-in root-tmp-dir tmp-file-count visited) -/// (let* ((tmp-dir (create-dir root-tmp-dir (make-hash))) -/// (new-files (create-in tmp-dir tmp-file-count (make-hash))) -/// (tmp-dir (create-dir tmp-dir (make-hash))) -/// (new-files (create-in tmp-dir tmp-file-count (make-hash)))) +/// (let (tmp-dir (create-dir root-tmp-dir {}) +/// new-files (create-in tmp-dir tmp-file-count {}) +/// tmp-dir (create-dir tmp-dir {}) +/// new-files (create-in tmp-dir tmp-file-count {})) /// (fs-crawl root-tmp-dir (fn (x) -/// (let ((file (hash-get visited x))) +/// (let (file visited.~x) /// (test::assert-true (not file)) ;; also tests double counting -/// (hash-set! visited x #t) -/// (set! cnt (+ 1 cnt)))) 1) +/// (set! visited.~x #t) +/// (inc! cnt))) 1) /// (test::assert-equal (+ 2 tmp-file-count) cnt) /// (test::assert-equal (+ 2 tmp-file-count) (length (hash-keys visited))) -/// (iterator::map (fn (x) (test::assert-true (hash-get visited y))) (hash-keys visited)))))) +/// (seq-for key in (hash-keys visited) (test::assert-true visited.~key))))) +/// |# #[sl_sh_fn(fn_name = "fs-crawl", takes_env = true)] fn fs_crawl( environment: &mut SloshVm, @@ -436,7 +439,15 @@ fn fs_crawl( Value::Keyword(i) if environment.get_interned(i) == "follow-syms" => { sym_links = Some(true); } - _ => return Err(VMError::new("io", format!("invalid argument {}", depth_or_symlink.display_value(environment)))), + _ => { + return Err(VMError::new( + "io", + format!( + "invalid argument {}", + depth_or_symlink.display_value(environment) + ), + )) + } } } match lambda_exp { @@ -446,8 +457,7 @@ fn fs_crawl( let path = entry.path(); if let Some(path) = path.to_str() { let path = environment.alloc_string(path.to_string()); - // XXXX make a call... - //call_lambda(environment, lambda_exp, &[path])?; + apply_callable(environment, lambda_exp, &[path])?; } Ok(()) }; @@ -498,7 +508,6 @@ fn fs_crawl( } } } - */ /// Usage: (fs-len /path/to/file/or/dir) /// @@ -650,7 +659,7 @@ pub fn add_fs_meta_builtins(env: &mut SloshVm) { intern_is_file(env); intern_is_dir(env); intern_do_glob(env); - //intern_fs_crawl(env); + intern_fs_crawl(env); intern_is_same_file(env); intern_fs_base(env); intern_fs_parent(env); diff --git a/compiler/src/load_eval.rs b/compiler/src/load_eval.rs index 012a794386..48b3e562a2 100644 --- a/compiler/src/load_eval.rs +++ b/compiler/src/load_eval.rs @@ -288,7 +288,7 @@ fn contains_list(args: &[Value]) -> bool { } /// Call lambda with args, this is re-entrant. -fn apply_callable(vm: &mut SloshVm, lambda: Value, args: &[Value]) -> VMResult { +pub fn apply_callable(vm: &mut SloshVm, lambda: Value, args: &[Value]) -> VMResult { match lambda { Value::Symbol(i) | Value::Special(i) if i == vm.specials().quote => { if let Some(arg) = args.first() {