1use runmat_builtins::{builtin_functions, lookup_method, Value};
7use runmat_macros::runtime_builtin;
8
9use crate::builtins::common::fs::contains_wildcards;
10use crate::builtins::common::path_search::{
11 class_file_exists as path_class_file_exists,
12 class_folder_candidates as path_class_folder_candidates,
13 directory_candidates as path_directory_candidates,
14 find_file_with_extensions as path_find_file_with_extensions, path_is_directory,
15 CLASS_M_FILE_EXTENSIONS, GENERAL_FILE_EXTENSIONS, LIB_EXTENSIONS, MEX_EXTENSIONS,
16 PCODE_EXTENSIONS, SIMULINK_EXTENSIONS, THUNK_EXTENSIONS,
17};
18use crate::builtins::common::spec::{
19 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
20 ReductionNaN, ResidencyPolicy, ShapeRequirements,
21};
22use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
23
24const ERROR_NAME_ARG: &str = "exist: name must be a character vector or string scalar";
25const ERROR_TYPE_ARG: &str = "exist: type must be a character vector or string scalar";
26const ERROR_INVALID_TYPE: &str = "exist: invalid type. Type must be one of 'var', 'variable', 'file', 'dir', 'directory', 'folder', 'builtin', 'built-in', 'class', 'handle', 'method', 'mex', 'pcode', 'simulink', 'thunk', 'lib', 'library', or 'java'";
27
28#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::exist")]
29pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
30 name: "exist",
31 op_kind: GpuOpKind::Custom("io"),
32 supported_precisions: &[],
33 broadcast: BroadcastSemantics::None,
34 provider_hooks: &[],
35 constant_strategy: ConstantStrategy::InlineLiteral,
36 residency: ResidencyPolicy::GatherImmediately,
37 nan_mode: ReductionNaN::Include,
38 two_pass_threshold: None,
39 workgroup_size: None,
40 accepts_nan_mode: false,
41 notes: "Filesystem and workspace lookup run on the host; arguments are gathered from the GPU when necessary.",
42};
43
44#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::exist")]
45pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
46 name: "exist",
47 shape: ShapeRequirements::Any,
48 constant_strategy: ConstantStrategy::InlineLiteral,
49 elementwise: None,
50 reduction: None,
51 emits_nan: false,
52 notes: "I/O builtins are not eligible for fusion; metadata registered for completeness.",
53};
54
55const BUILTIN_NAME: &str = "exist";
56
57fn exist_error(message: impl Into<String>) -> RuntimeError {
58 build_runtime_error(message)
59 .with_builtin(BUILTIN_NAME)
60 .build()
61}
62
63fn map_control_flow(err: RuntimeError) -> RuntimeError {
64 let identifier = err.identifier().map(str::to_string);
65 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
66 .with_builtin(BUILTIN_NAME)
67 .with_source(err);
68 if let Some(identifier) = identifier {
69 builder = builder.with_identifier(identifier);
70 }
71 builder.build()
72}
73
74#[runtime_builtin(
75 name = "exist",
76 category = "io/repl_fs",
77 summary = "Determine whether a variable, file, folder, built-in, or class exists.",
78 keywords = "exist,file,dir,var,builtin,class",
79 accel = "cpu",
80 type_resolver(crate::builtins::io::type_resolvers::exist_type),
81 builtin_path = "crate::builtins::io::repl_fs::exist"
82)]
83async fn exist_builtin(name: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
84 if rest.len() > 1 {
85 return Err(exist_error("exist: too many input arguments"));
86 }
87
88 let name_host = gather_if_needed_async(&name)
89 .await
90 .map_err(map_control_flow)?;
91 let type_value = match rest.first() {
92 Some(value) => Some(
93 gather_if_needed_async(value)
94 .await
95 .map_err(map_control_flow)?,
96 ),
97 None => None,
98 };
99
100 let query = type_value
101 .as_ref()
102 .map(parse_type_argument)
103 .transpose()?
104 .unwrap_or(ExistQuery::Any);
105
106 let result = match query {
107 ExistQuery::Handle => exist_handle(&name_host),
108 _ => {
109 let text = value_to_string(&name_host).ok_or_else(|| exist_error(ERROR_NAME_ARG))?;
110 exist_for_query(&text, query).await?
111 }
112 };
113
114 Ok(Value::Num(result.code()))
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118enum ExistQuery {
119 Any,
120 Var,
121 File,
122 Dir,
123 Builtin,
124 Class,
125 Mex,
126 Pcode,
127 Method,
128 Handle,
129 Simulink,
130 Thunk,
131 Lib,
132 Java,
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136enum ExistResultKind {
137 NotFound,
138 Variable,
139 File,
140 Mex,
141 Simulink,
142 Builtin,
143 Pcode,
144 Directory,
145 Class,
146}
147
148impl ExistResultKind {
149 fn code(self) -> f64 {
150 match self {
151 ExistResultKind::NotFound => 0.0,
152 ExistResultKind::Variable => 1.0,
153 ExistResultKind::File => 2.0,
154 ExistResultKind::Mex => 3.0,
155 ExistResultKind::Simulink => 4.0,
156 ExistResultKind::Builtin => 5.0,
157 ExistResultKind::Pcode => 6.0,
158 ExistResultKind::Directory => 7.0,
159 ExistResultKind::Class => 8.0,
160 }
161 }
162}
163
164fn parse_type_argument(value: &Value) -> BuiltinResult<ExistQuery> {
165 let text = value_to_string(value).ok_or_else(|| exist_error(ERROR_TYPE_ARG))?;
166 match text.trim().to_ascii_lowercase().as_str() {
167 "" => Ok(ExistQuery::Any),
168 "var" | "variable" => Ok(ExistQuery::Var),
169 "file" => Ok(ExistQuery::File),
170 "dir" | "directory" | "folder" => Ok(ExistQuery::Dir),
171 "builtin" | "built-in" => Ok(ExistQuery::Builtin),
172 "class" => Ok(ExistQuery::Class),
173 "mex" => Ok(ExistQuery::Mex),
174 "pcode" | "p" => Ok(ExistQuery::Pcode),
175 "handle" => Ok(ExistQuery::Handle),
176 "method" => Ok(ExistQuery::Method),
177 "simulink" => Ok(ExistQuery::Simulink),
178 "thunk" => Ok(ExistQuery::Thunk),
179 "lib" | "library" => Ok(ExistQuery::Lib),
180 "java" => Ok(ExistQuery::Java),
181 _ => Err(exist_error(ERROR_INVALID_TYPE)),
182 }
183}
184
185async fn exist_for_query(name: &str, query: ExistQuery) -> BuiltinResult<ExistResultKind> {
186 if contains_wildcards(name) {
187 return Ok(ExistResultKind::NotFound);
188 }
189
190 match query {
191 ExistQuery::Any => evaluate_default(name).await,
192 ExistQuery::Var => Ok(if variable_exists(name) {
193 ExistResultKind::Variable
194 } else {
195 ExistResultKind::NotFound
196 }),
197 ExistQuery::Builtin => Ok(if builtin_exists(name) {
198 ExistResultKind::Builtin
199 } else {
200 ExistResultKind::NotFound
201 }),
202 ExistQuery::Class => Ok(if class_exists(name).await? {
203 ExistResultKind::Class
204 } else {
205 ExistResultKind::NotFound
206 }),
207 ExistQuery::Dir => Ok(if directory_exists(name).await? {
208 ExistResultKind::Directory
209 } else {
210 ExistResultKind::NotFound
211 }),
212 ExistQuery::File => Ok(detect_file_kind(name)
213 .await?
214 .unwrap_or(ExistResultKind::NotFound)),
215 ExistQuery::Mex => Ok(
216 if path_find_file_with_extensions(name, MEX_EXTENSIONS, "exist")
217 .await
218 .map_err(exist_error)?
219 .is_some()
220 {
221 ExistResultKind::Mex
222 } else {
223 ExistResultKind::NotFound
224 },
225 ),
226 ExistQuery::Pcode => Ok(
227 if path_find_file_with_extensions(name, PCODE_EXTENSIONS, "exist")
228 .await
229 .map_err(exist_error)?
230 .is_some()
231 {
232 ExistResultKind::Pcode
233 } else {
234 ExistResultKind::NotFound
235 },
236 ),
237 ExistQuery::Method => Ok(if method_exists(name) {
238 ExistResultKind::Builtin
239 } else {
240 ExistResultKind::NotFound
241 }),
242 ExistQuery::Simulink => Ok(
243 if path_find_file_with_extensions(name, SIMULINK_EXTENSIONS, "exist")
244 .await
245 .map_err(exist_error)?
246 .is_some()
247 {
248 ExistResultKind::Simulink
249 } else {
250 ExistResultKind::NotFound
251 },
252 ),
253 ExistQuery::Thunk => Ok(
254 if path_find_file_with_extensions(name, THUNK_EXTENSIONS, "exist")
255 .await
256 .map_err(exist_error)?
257 .is_some()
258 {
259 ExistResultKind::File
260 } else {
261 ExistResultKind::NotFound
262 },
263 ),
264 ExistQuery::Lib => Ok(
265 if path_find_file_with_extensions(name, LIB_EXTENSIONS, "exist")
266 .await
267 .map_err(exist_error)?
268 .is_some()
269 {
270 ExistResultKind::File
271 } else {
272 ExistResultKind::NotFound
273 },
274 ),
275 ExistQuery::Java => Ok(ExistResultKind::NotFound),
276 ExistQuery::Handle => unreachable!("handle queries handled separately"),
277 }
278}
279
280async fn evaluate_default(name: &str) -> BuiltinResult<ExistResultKind> {
281 if variable_exists(name) {
282 return Ok(ExistResultKind::Variable);
283 }
284 if builtin_exists(name) {
285 return Ok(ExistResultKind::Builtin);
286 }
287 if class_exists(name).await? {
288 return Ok(ExistResultKind::Class);
289 }
290 if let Some(kind) = detect_file_kind(name).await? {
291 return Ok(kind);
292 }
293 if directory_exists(name).await? {
294 return Ok(ExistResultKind::Directory);
295 }
296 Ok(ExistResultKind::NotFound)
297}
298
299fn exist_handle(value: &Value) -> ExistResultKind {
300 match value {
301 Value::HandleObject(handle) => {
302 if handle.valid {
303 ExistResultKind::Variable
304 } else {
305 ExistResultKind::NotFound
306 }
307 }
308 Value::Listener(listener) => {
309 if listener.valid {
310 ExistResultKind::Variable
311 } else {
312 ExistResultKind::NotFound
313 }
314 }
315 _ => ExistResultKind::NotFound,
316 }
317}
318
319fn variable_exists(name: &str) -> bool {
320 crate::workspace::lookup(name).is_some()
321}
322
323fn builtin_exists(name: &str) -> bool {
324 let lowered = name.to_ascii_lowercase();
325 builtin_functions()
326 .into_iter()
327 .any(|b| b.name.eq_ignore_ascii_case(&lowered))
328}
329
330async fn class_exists(name: &str) -> BuiltinResult<bool> {
331 if runmat_builtins::get_class(name).is_some() {
332 return Ok(true);
333 }
334 if class_folder_exists(name).await? {
335 return Ok(true);
336 }
337 if class_file_exists(name).await? {
338 return Ok(true);
339 }
340 Ok(false)
341}
342
343async fn class_folder_exists(name: &str) -> BuiltinResult<bool> {
344 for path in path_class_folder_candidates(name, "exist").map_err(exist_error)? {
345 if path_is_directory(&path).await {
346 return Ok(true);
347 }
348 }
349 Ok(false)
350}
351
352async fn class_file_exists(name: &str) -> BuiltinResult<bool> {
353 path_class_file_exists(name, CLASS_M_FILE_EXTENSIONS, "classdef", "exist")
354 .await
355 .map_err(exist_error)
356}
357
358fn method_exists(name: &str) -> bool {
359 if let Some((class_name, method_name)) = split_method_name(name) {
360 lookup_method(&class_name, &method_name).is_some()
361 } else {
362 false
363 }
364}
365
366async fn directory_exists(name: &str) -> BuiltinResult<bool> {
367 for path in path_directory_candidates(name, "exist").map_err(exist_error)? {
368 if path_is_directory(&path).await {
369 return Ok(true);
370 }
371 }
372 Ok(false)
373}
374
375async fn detect_file_kind(name: &str) -> BuiltinResult<Option<ExistResultKind>> {
376 if path_find_file_with_extensions(name, MEX_EXTENSIONS, "exist")
377 .await
378 .map_err(exist_error)?
379 .is_some()
380 {
381 return Ok(Some(ExistResultKind::Mex));
382 }
383 if path_find_file_with_extensions(name, PCODE_EXTENSIONS, "exist")
384 .await
385 .map_err(exist_error)?
386 .is_some()
387 {
388 return Ok(Some(ExistResultKind::Pcode));
389 }
390 if path_find_file_with_extensions(name, SIMULINK_EXTENSIONS, "exist")
391 .await
392 .map_err(exist_error)?
393 .is_some()
394 {
395 return Ok(Some(ExistResultKind::Simulink));
396 }
397 if path_find_file_with_extensions(name, GENERAL_FILE_EXTENSIONS, "exist")
398 .await
399 .map_err(exist_error)?
400 .is_some()
401 {
402 return Ok(Some(ExistResultKind::File));
403 }
404 Ok(None)
405}
406
407fn value_to_string(value: &Value) -> Option<String> {
408 match value {
409 Value::String(text) => Some(text.clone()),
410 Value::CharArray(array) if array.rows == 1 => Some(array.data.iter().collect()),
411 Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
412 _ => None,
413 }
414}
415
416fn split_method_name(name: &str) -> Option<(String, String)> {
417 let mut parts: Vec<&str> = name.split('.').collect();
418 if parts.len() < 2 {
419 return None;
420 }
421 let method = parts.pop()?.to_string();
422 if method.is_empty() {
423 return None;
424 }
425 let class = parts.join(".");
426 if class.is_empty() {
427 return None;
428 }
429 Some((class, method))
430}
431
432#[cfg(test)]
433pub(crate) mod tests {
434 use super::super::REPL_FS_TEST_LOCK;
435 use super::*;
436 use runmat_builtins::Value;
437 use runmat_filesystem as vfs;
438 use runmat_thread_local::runmat_thread_local;
439 use std::cell::RefCell;
440 use std::collections::HashMap;
441 use std::env;
442 use std::fs::File;
443 use std::io::Write;
444 use std::path::PathBuf;
445 use tempfile::tempdir;
446
447 fn exist_builtin(name: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
448 futures::executor::block_on(super::exist_builtin(name, rest))
449 }
450
451 fn workspace_guard() -> std::sync::MutexGuard<'static, ()> {
452 crate::workspace::test_guard()
453 }
454
455 fn test_guard() -> (
456 std::sync::MutexGuard<'static, ()>,
457 std::sync::MutexGuard<'static, ()>,
458 ) {
459 let workspace = workspace_guard();
460 let fs_lock = REPL_FS_TEST_LOCK
461 .lock()
462 .unwrap_or_else(|poison| poison.into_inner());
463 (workspace, fs_lock)
464 }
465
466 runmat_thread_local! {
467 static TEST_WORKSPACE: RefCell<HashMap<String, Value>> = RefCell::new(HashMap::new());
468 }
469
470 fn ensure_test_resolver() {
471 crate::workspace::register_workspace_resolver(crate::workspace::WorkspaceResolver {
472 lookup: |name| TEST_WORKSPACE.with(|slot| slot.borrow().get(name).cloned()),
473 snapshot: || {
474 let mut entries: Vec<(String, Value)> =
475 TEST_WORKSPACE.with(|slot| slot.borrow().clone().into_iter().collect());
476 entries.sort_by(|a, b| a.0.cmp(&b.0));
477 entries
478 },
479 globals: || Vec::new(),
480 assign: None,
481 clear: None,
482 remove: None,
483 });
484 }
485
486 fn set_workspace(entries: &[(&str, Value)]) {
487 TEST_WORKSPACE.with(|slot| {
488 let mut map = slot.borrow_mut();
489 map.clear();
490 for (name, value) in entries {
491 map.insert((*name).to_string(), value.clone());
492 }
493 });
494 }
495
496 struct DirGuard {
497 original: PathBuf,
498 }
499
500 impl DirGuard {
501 fn new() -> Self {
502 let original = env::current_dir().expect("current dir");
503 Self { original }
504 }
505 }
506
507 impl Drop for DirGuard {
508 fn drop(&mut self) {
509 let _ = env::set_current_dir(&self.original);
510 }
511 }
512
513 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
514 #[test]
515 fn exist_detects_workspace_variables() {
516 let (_guard, _lock) = test_guard();
517 ensure_test_resolver();
518 set_workspace(&[("alpha", Value::Num(1.0))]);
519
520 let value = exist_builtin(Value::from("alpha"), Vec::new()).expect("exist");
521 assert_eq!(value, Value::Num(1.0));
522 }
523
524 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
525 #[test]
526 fn exist_detects_builtins() {
527 let (_guard, _lock) = test_guard();
528
529 let value = exist_builtin(Value::from("sin"), Vec::new()).expect("exist");
530 assert_eq!(value, Value::Num(5.0));
531
532 let builtin =
533 exist_builtin(Value::from("sin"), vec![Value::from("builtin")]).expect("exist");
534 assert_eq!(builtin, Value::Num(5.0));
535 }
536
537 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
538 #[test]
539 fn exist_detects_files_and_mex() {
540 let (_guard, _lock) = test_guard();
541 ensure_test_resolver();
542
543 let temp = tempdir().expect("tempdir");
544 let _guard = DirGuard::new();
545 env::set_current_dir(temp.path()).expect("set temp");
546
547 File::create("script.m").expect("create m-file");
548 File::create("fastfft.mexw64").expect("create mex");
549
550 let script =
551 exist_builtin(Value::from("script"), vec![Value::from("file")]).expect("exist");
552 assert_eq!(script, Value::Num(2.0));
553
554 let mex = exist_builtin(Value::from("fastfft"), vec![Value::from("file")]).expect("exist");
555 assert_eq!(mex, Value::Num(3.0));
556
557 let mex_specific =
558 exist_builtin(Value::from("fastfft"), vec![Value::from("mex")]).expect("exist");
559 assert_eq!(mex_specific, Value::Num(3.0));
560 }
561
562 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
563 #[test]
564 fn exist_detects_directories() {
565 let (_guard, _lock) = test_guard();
566
567 let temp = tempdir().expect("tempdir");
568 let _guard = DirGuard::new();
569 env::set_current_dir(temp.path()).expect("set temp");
570 futures::executor::block_on(vfs::create_dir_async("data")).expect("mkdir data");
571
572 let dir = exist_builtin(Value::from("data"), vec![Value::from("dir")]).expect("exist");
573 assert_eq!(dir, Value::Num(7.0));
574
575 let any = exist_builtin(Value::from("data"), Vec::new()).expect("exist");
576 assert_eq!(any, Value::Num(7.0));
577 }
578
579 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
580 #[test]
581 fn exist_detects_class_files_and_packages() {
582 let (_guard, _lock) = test_guard();
583
584 let temp = tempdir().expect("tempdir");
585 let _guard = DirGuard::new();
586 env::set_current_dir(temp.path()).expect("set temp");
587
588 let mut file = File::create("Widget.m").expect("create class file");
589 writeln!(
590 file,
591 "classdef Widget\n methods\n function obj = Widget()\n end\n end\nend"
592 )
593 .expect("write classdef");
594
595 futures::executor::block_on(vfs::create_dir_all_async("+pkg/@Gizmo"))
596 .expect("create package class folder");
597
598 let widget =
599 exist_builtin(Value::from("Widget"), vec![Value::from("class")]).expect("exist");
600 assert_eq!(widget, Value::Num(8.0));
601
602 let gizmo =
603 exist_builtin(Value::from("pkg.Gizmo"), vec![Value::from("class")]).expect("exist pkg");
604 assert_eq!(gizmo, Value::Num(8.0));
605 }
606
607 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
608 #[test]
609 fn exist_invalid_type_raises_error() {
610 let (_guard, _lock) = test_guard();
611
612 let err = exist_builtin(Value::from("foo"), vec![Value::from("unknown")])
613 .expect_err("expected error");
614 assert_eq!(err.message(), ERROR_INVALID_TYPE);
615 }
616
617 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
618 #[test]
619 fn exist_errors_on_non_text_name() {
620 let (_guard, _lock) = test_guard();
621
622 let err = exist_builtin(Value::Num(5.0), Vec::new()).expect_err("expected error");
623 assert_eq!(err.message(), ERROR_NAME_ARG);
624 }
625
626 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
627 #[test]
628 fn exist_handle_returns_zero_for_non_handle() {
629 let (_guard, _lock) = test_guard();
630
631 let value =
632 exist_builtin(Value::Num(17.0), vec![Value::from("handle")]).expect("exist handle");
633 assert_eq!(value, Value::Num(0.0));
634 }
635}