runmat_runtime/builtins/io/repl_fs/
tempdir.rs1use crate::builtins::common::env as runtime_env;
4use std::convert::TryFrom;
5use std::path::Path;
6
7use runmat_builtins::{CharArray, Value};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::spec::{
11 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12 ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14use crate::{build_runtime_error, RuntimeError};
15
16const ERR_TOO_MANY_INPUTS: &str = "tempdir: too many input arguments";
17const ERR_UNABLE_TO_DETERMINE: &str =
18 "tempdir: unable to determine temporary directory (OS returned empty path)";
19
20#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::tempdir")]
21pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
22 name: "tempdir",
23 op_kind: GpuOpKind::Custom("io"),
24 supported_precisions: &[],
25 broadcast: BroadcastSemantics::None,
26 provider_hooks: &[],
27 constant_strategy: ConstantStrategy::InlineLiteral,
28 residency: ResidencyPolicy::GatherImmediately,
29 nan_mode: ReductionNaN::Include,
30 two_pass_threshold: None,
31 workgroup_size: None,
32 accepts_nan_mode: false,
33 notes: "Host-only operation that queries the environment for the temporary folder. No provider hooks are required.",
34};
35
36#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::tempdir")]
37pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
38 name: "tempdir",
39 shape: ShapeRequirements::Any,
40 constant_strategy: ConstantStrategy::InlineLiteral,
41 elementwise: None,
42 reduction: None,
43 emits_nan: false,
44 notes: "I/O builtin that always executes on the host; fusion metadata is present for introspection completeness.",
45};
46
47const BUILTIN_NAME: &str = "tempdir";
48
49fn tempdir_error(message: impl Into<String>) -> RuntimeError {
50 build_runtime_error(message)
51 .with_builtin(BUILTIN_NAME)
52 .build()
53}
54
55#[runtime_builtin(
56 name = "tempdir",
57 category = "io/repl_fs",
58 summary = "Return the absolute path to the system temporary folder.",
59 keywords = "tempdir,temporary folder,temp directory,system temp",
60 accel = "cpu",
61 type_resolver(crate::builtins::io::type_resolvers::tempdir_type),
62 builtin_path = "crate::builtins::io::repl_fs::tempdir"
63)]
64async fn tempdir_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
65 if !args.is_empty() {
66 return Err(tempdir_error(ERR_TOO_MANY_INPUTS));
67 }
68 let path = runtime_env::temp_dir();
69 if path.as_os_str().is_empty() {
70 return Err(tempdir_error(ERR_UNABLE_TO_DETERMINE));
71 }
72 let value = path_to_char_array(&path);
73 if let Ok(text) = String::try_from(&value) {
74 if text.is_empty() {
75 return Err(tempdir_error(ERR_UNABLE_TO_DETERMINE));
76 }
77 }
78 Ok(value)
79}
80
81fn path_to_char_array(path: &Path) -> Value {
82 let mut text = path.to_string_lossy().into_owned();
83 if !text.is_empty() && !ends_with_separator(&text) {
84 text.push(std::path::MAIN_SEPARATOR);
85 }
86 Value::CharArray(CharArray::new_row(&text))
87}
88
89fn ends_with_separator(text: &str) -> bool {
90 let sep = std::path::MAIN_SEPARATOR;
91 text.chars()
92 .next_back()
93 .is_some_and(|ch| ch == sep || (cfg!(windows) && (ch == '/' || ch == '\\')))
94}
95
96#[cfg(test)]
97pub(crate) mod tests {
98 use super::*;
99 use crate::BuiltinResult;
100 use std::convert::TryFrom;
101 use std::path::Path;
102
103 fn tempdir_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
104 futures::executor::block_on(super::tempdir_builtin(args))
105 }
106
107 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
108 #[test]
109 fn tempdir_points_to_existing_directory() {
110 let value = tempdir_builtin(Vec::new()).expect("tempdir");
111 let path_string = String::try_from(&value).expect("convert to string");
112 let _path = Path::new(&path_string);
113 }
114
115 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
116 #[test]
117 fn tempdir_returns_char_array_row_vector() {
118 let value = tempdir_builtin(Vec::new()).expect("tempdir");
119 match value {
120 Value::CharArray(CharArray { rows, cols, .. }) => {
121 assert_eq!(rows, 1);
122 assert!(
123 cols >= 1,
124 "expected tempdir to return at least one character"
125 );
126 }
127 other => panic!("expected CharArray result, got {other:?}"),
128 }
129 }
130
131 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
132 #[test]
133 fn tempdir_appends_trailing_separator() {
134 let value = tempdir_builtin(Vec::new()).expect("tempdir");
135 let path_string = String::try_from(&value).expect("convert to string");
136 let expected_sep = std::path::MAIN_SEPARATOR;
137 let last = path_string
138 .chars()
139 .last()
140 .expect("tempdir should return non-empty path");
141 if cfg!(windows) {
142 assert!(
143 last == '/' || last == '\\',
144 "expected trailing separator, got {:?}",
145 path_string
146 );
147 } else {
148 assert_eq!(
149 last, expected_sep,
150 "expected trailing separator {}, got {}",
151 expected_sep, path_string
152 );
153 }
154 }
155
156 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
157 #[test]
158 fn tempdir_returns_consistent_result() {
159 let first = tempdir_builtin(Vec::new()).expect("tempdir");
160 let second = tempdir_builtin(Vec::new()).expect("tempdir");
161 let first_str = String::try_from(&first).expect("first string");
162 let second_str = String::try_from(&second).expect("second string");
163 assert_eq!(
164 first_str, second_str,
165 "tempdir should be stable within a process"
166 );
167 }
168
169 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
170 #[test]
171 fn tempdir_errors_when_arguments_provided() {
172 let err = tempdir_builtin(vec![Value::Num(1.0)]).expect_err("expected error");
173 assert_eq!(err.message(), ERR_TOO_MANY_INPUTS);
174 }
175
176 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
177 #[test]
178 fn path_to_char_array_appends_separator_when_missing() {
179 let path = Path::new("runmat_tempdir_unit_path");
180 let value = path_to_char_array(path);
181 let text = String::try_from(&value).expect("string conversion");
182 assert!(
183 text.ends_with(std::path::MAIN_SEPARATOR),
184 "expected trailing separator in {text:?}"
185 );
186 let trimmed = text.trim_end_matches(std::path::MAIN_SEPARATOR);
187 assert_eq!(trimmed, path.to_string_lossy());
188 }
189
190 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
191 #[test]
192 fn path_to_char_array_preserves_existing_separator() {
193 let sep = std::path::MAIN_SEPARATOR;
194 let input = format!("runmat_tempdir_existing{sep}");
195 let path = Path::new(&input);
196 let value = path_to_char_array(path);
197 let text = String::try_from(&value).expect("string conversion");
198 assert_eq!(text, input);
199 }
200
201 #[cfg(windows)]
202 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
203 #[test]
204 fn ends_with_separator_accepts_forward_slash() {
205 assert!(ends_with_separator("C:/Temp/"));
206 assert!(ends_with_separator("temp/"));
207 }
208}