1use runmat_builtins::{
2 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
3 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
4 CellArray, StructValue, Tensor, Value,
5};
6use runmat_macros::runtime_builtin;
7use runmat_thread_local::runmat_thread_local;
8use std::cell::RefCell;
9use std::collections::HashSet;
10use std::path::{Path, PathBuf};
11
12use crate::builtins::common::path_search::{file_candidates, path_is_file};
13use crate::console::{record_console_line, ConsoleStream};
14use crate::{BuiltinResult, RuntimeError};
15
16const DBSTACK_OUTPUTS: [BuiltinParamDescriptor; 2] = [
17 BuiltinParamDescriptor {
18 name: "ST",
19 ty: BuiltinParamType::Any,
20 arity: BuiltinParamArity::Required,
21 default: None,
22 description: "Call-stack entries as a 1-by-N cell row of scalar structs.",
23 },
24 BuiltinParamDescriptor {
25 name: "I",
26 ty: BuiltinParamType::NumericScalar,
27 arity: BuiltinParamArity::Optional,
28 default: None,
29 description: "Current workspace index within the returned stack.",
30 },
31];
32
33const DBSTACK_STACK_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
34 name: "ST",
35 ty: BuiltinParamType::Any,
36 arity: BuiltinParamArity::Required,
37 default: None,
38 description: "Call-stack entries as a 1-by-N cell row of scalar structs.",
39}];
40
41const DBSTACK_INPUTS: [BuiltinParamDescriptor; 2] = [
42 BuiltinParamDescriptor {
43 name: "n",
44 ty: BuiltinParamType::IntegerScalar,
45 arity: BuiltinParamArity::Optional,
46 default: None,
47 description: "Number of leading stack entries to omit.",
48 },
49 BuiltinParamDescriptor {
50 name: "option",
51 ty: BuiltinParamType::StringScalar,
52 arity: BuiltinParamArity::Optional,
53 default: None,
54 description: "`-completenames` is accepted for MATLAB command compatibility.",
55 },
56];
57
58const DBSTACK_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
59 BuiltinSignatureDescriptor {
60 label: "dbstack",
61 inputs: &[],
62 outputs: &[],
63 },
64 BuiltinSignatureDescriptor {
65 label: "ST = dbstack(n, '-completenames')",
66 inputs: &DBSTACK_INPUTS,
67 outputs: &DBSTACK_STACK_OUTPUT,
68 },
69 BuiltinSignatureDescriptor {
70 label: "[ST,I] = dbstack(...)",
71 inputs: &DBSTACK_INPUTS,
72 outputs: &DBSTACK_OUTPUTS,
73 },
74];
75
76const TEXT_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
77 name: "name",
78 ty: BuiltinParamType::StringScalar,
79 arity: BuiltinParamArity::Optional,
80 default: None,
81 description: "Function, script, file, command, or option text.",
82}];
83
84const DBTYPE_INPUTS: [BuiltinParamDescriptor; 2] = [
85 BuiltinParamDescriptor {
86 name: "file",
87 ty: BuiltinParamType::StringScalar,
88 arity: BuiltinParamArity::Required,
89 default: None,
90 description: "MATLAB source file, path, or function name.",
91 },
92 BuiltinParamDescriptor {
93 name: "range",
94 ty: BuiltinParamType::StringScalar,
95 arity: BuiltinParamArity::Optional,
96 default: None,
97 description: "Optional line range such as `3:8`.",
98 },
99];
100
101const DBTYPE_FILE_INPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
102 name: "file",
103 ty: BuiltinParamType::StringScalar,
104 arity: BuiltinParamArity::Required,
105 default: None,
106 description: "MATLAB source file, path, or function name.",
107}];
108
109const DBTYPE_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
110 BuiltinSignatureDescriptor {
111 label: "dbtype file",
112 inputs: &DBTYPE_FILE_INPUT,
113 outputs: &[],
114 },
115 BuiltinSignatureDescriptor {
116 label: "dbtype file start:end",
117 inputs: &DBTYPE_INPUTS,
118 outputs: &[],
119 },
120];
121
122const SIMPLE_NO_OUTPUT_SIGNATURE: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
123 label: "keyboard",
124 inputs: &[],
125 outputs: &[],
126}];
127
128const MLOCK_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
129 label: "mlock",
130 inputs: &[],
131 outputs: &[],
132}];
133
134const MUNLOCK_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
135 BuiltinSignatureDescriptor {
136 label: "munlock",
137 inputs: &[],
138 outputs: &[],
139 },
140 BuiltinSignatureDescriptor {
141 label: "munlock(fun)",
142 inputs: &TEXT_INPUT,
143 outputs: &[],
144 },
145];
146
147const MISLOCKED_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
148 name: "tf",
149 ty: BuiltinParamType::Any,
150 arity: BuiltinParamArity::Required,
151 default: None,
152 description: "Logical scalar indicating whether the function or script is locked.",
153}];
154
155const MISLOCKED_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
156 BuiltinSignatureDescriptor {
157 label: "tf = mislocked",
158 inputs: &[],
159 outputs: &MISLOCKED_OUTPUT,
160 },
161 BuiltinSignatureDescriptor {
162 label: "tf = mislocked(fun)",
163 inputs: &TEXT_INPUT,
164 outputs: &MISLOCKED_OUTPUT,
165 },
166];
167
168const DBSTATUS_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
169 BuiltinSignatureDescriptor {
170 label: "dbstatus",
171 inputs: &TEXT_INPUT,
172 outputs: &[],
173 },
174 BuiltinSignatureDescriptor {
175 label: "b = dbstatus(file, '-completenames')",
176 inputs: &TEXT_INPUT,
177 outputs: &DBSTATUS_OUTPUT,
178 },
179];
180
181const DBSTATUS_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
182 name: "b",
183 ty: BuiltinParamType::Any,
184 arity: BuiltinParamArity::Required,
185 default: None,
186 description: "Breakpoint entries as a 1-by-N cell row; currently empty until execution breakpoints are implemented.",
187}];
188
189const DBCLEAR_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
190 label: "dbclear all | dbclear in file at location | dbclear if condition",
191 inputs: &TEXT_INPUT,
192 outputs: &[],
193}];
194
195const GETCALLINFO_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
196 label: "info = getcallinfo",
197 inputs: &[],
198 outputs: &GETCALLINFO_OUTPUT,
199}];
200
201const GETCALLINFO_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
202 name: "info",
203 ty: BuiltinParamType::Any,
204 arity: BuiltinParamArity::Required,
205 default: None,
206 description: "Scalar struct describing the current call context and stack.",
207}];
208
209pub const DEBUG_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
210 code: "RM.DEBUG.TOO_MANY_INPUTS",
211 identifier: Some("RunMat:TooManyInputs"),
212 when: "A debugger compatibility helper receives more inputs than it supports.",
213 message: "debugger helper: too many input arguments",
214};
215
216pub const DEBUG_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
217 code: "RM.DEBUG.INVALID_INPUT",
218 identifier: Some("RunMat:InvalidInput"),
219 when: "An input is not a supported text, scalar, or option value.",
220 message: "debugger helper: invalid input",
221};
222
223pub const DEBUG_ERROR_NO_CURRENT_FILE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
224 code: "RM.DEBUG.NO_CURRENT_FILE",
225 identifier: Some("RunMat:NoCurrentFile"),
226 when: "`mlock`, `munlock`, or `mislocked` is called without a current function or script.",
227 message: "debugger helper: no current function or script is available",
228};
229
230pub const DEBUG_ERROR_FILE_NOT_FOUND: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
231 code: "RM.DEBUG.FILE_NOT_FOUND",
232 identifier: Some("RunMat:FileNotFound"),
233 when: "`dbtype` cannot resolve a MATLAB source file.",
234 message: "dbtype: file not found",
235};
236
237pub const DEBUG_ERROR_FILE_READ: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
238 code: "RM.DEBUG.FILE_READ",
239 identifier: Some("RunMat:FileReadFailed"),
240 when: "`dbtype` cannot read the resolved source file.",
241 message: "dbtype: failed to read file",
242};
243
244pub const DEBUG_ERRORS: [BuiltinErrorDescriptor; 5] = [
245 DEBUG_ERROR_TOO_MANY_INPUTS,
246 DEBUG_ERROR_INVALID_INPUT,
247 DEBUG_ERROR_NO_CURRENT_FILE,
248 DEBUG_ERROR_FILE_NOT_FOUND,
249 DEBUG_ERROR_FILE_READ,
250];
251
252pub const DBSTACK_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
253 signatures: &DBSTACK_SIGNATURES,
254 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
255 completion_policy: BuiltinCompletionPolicy::Public,
256 errors: &DEBUG_ERRORS,
257};
258
259pub const DBTYPE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
260 signatures: &DBTYPE_SIGNATURES,
261 output_mode: BuiltinOutputMode::Fixed,
262 completion_policy: BuiltinCompletionPolicy::Public,
263 errors: &DEBUG_ERRORS,
264};
265
266pub const KEYBOARD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
267 signatures: &SIMPLE_NO_OUTPUT_SIGNATURE,
268 output_mode: BuiltinOutputMode::Fixed,
269 completion_policy: BuiltinCompletionPolicy::Public,
270 errors: &[],
271};
272
273pub const MLOCK_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
274 signatures: &MLOCK_SIGNATURES,
275 output_mode: BuiltinOutputMode::Fixed,
276 completion_policy: BuiltinCompletionPolicy::Public,
277 errors: &DEBUG_ERRORS,
278};
279
280pub const MUNLOCK_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
281 signatures: &MUNLOCK_SIGNATURES,
282 output_mode: BuiltinOutputMode::Fixed,
283 completion_policy: BuiltinCompletionPolicy::Public,
284 errors: &DEBUG_ERRORS,
285};
286
287pub const MISLOCKED_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
288 signatures: &MISLOCKED_SIGNATURES,
289 output_mode: BuiltinOutputMode::Fixed,
290 completion_policy: BuiltinCompletionPolicy::Public,
291 errors: &DEBUG_ERRORS,
292};
293
294pub const DBSTATUS_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
295 signatures: &DBSTATUS_SIGNATURES,
296 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
297 completion_policy: BuiltinCompletionPolicy::Public,
298 errors: &DEBUG_ERRORS,
299};
300
301pub const DBCLEAR_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
302 signatures: &DBCLEAR_SIGNATURES,
303 output_mode: BuiltinOutputMode::Fixed,
304 completion_policy: BuiltinCompletionPolicy::Public,
305 errors: &DEBUG_ERRORS,
306};
307
308pub const GETCALLINFO_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
309 signatures: &GETCALLINFO_SIGNATURES,
310 output_mode: BuiltinOutputMode::Fixed,
311 completion_policy: BuiltinCompletionPolicy::Public,
312 errors: &DEBUG_ERRORS,
313};
314
315runmat_thread_local! {
316 static LOCKED_FUNCTIONS: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
317}
318
319pub fn reset_lock_registry_for_tests() {
320 LOCKED_FUNCTIONS.with(|locks| locks.borrow_mut().clear());
321}
322
323fn debug_error(
324 builtin: &'static str,
325 descriptor: &'static BuiltinErrorDescriptor,
326 detail: impl AsRef<str>,
327) -> RuntimeError {
328 let detail = detail.as_ref();
329 let message = if detail.is_empty() {
330 descriptor.message.to_string()
331 } else {
332 format!("{}: {detail}", descriptor.message)
333 };
334 let mut builder = crate::build_runtime_error(message).with_builtin(builtin);
335 if let Some(identifier) = descriptor.identifier {
336 builder = builder.with_identifier(identifier);
337 }
338 builder.build()
339}
340
341fn empty_value() -> Value {
342 Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("empty tensor"))
343}
344
345fn text_value(value: &Value) -> Option<String> {
346 match value {
347 Value::String(text) => Some(text.clone()),
348 Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
349 Value::CharArray(array) if array.rows == 1 => Some(array.data.iter().collect()),
350 _ => None,
351 }
352}
353
354fn integer_value(value: &Value) -> Option<usize> {
355 match value {
356 Value::Num(n) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => Some(*n as usize),
357 Value::Int(int) => usize::try_from(int.to_i64()).ok(),
358 _ => None,
359 }
360}
361
362fn stack_struct(frame: &crate::debug_context::DebugFrameInfo, index: usize) -> Value {
363 let mut st = StructValue::new();
364 st.insert("file", Value::String(frame.file.clone()));
365 st.insert("name", Value::String(frame.function.clone()));
366 st.insert("line", Value::Num(frame.line as f64));
367 st.insert("I", Value::Num(index as f64));
368 Value::Struct(st)
369}
370
371fn cell_row(values: Vec<Value>) -> BuiltinResult<Value> {
372 if values.is_empty() {
373 return Ok(Value::Cell(CellArray::new(Vec::new(), 0, 0).map_err(
374 |err| debug_error("debugger", &DEBUG_ERROR_INVALID_INPUT, &err),
375 )?));
376 }
377 let cols = values.len();
378 Ok(Value::Cell(CellArray::new(values, 1, cols).map_err(
379 |err| debug_error("debugger", &DEBUG_ERROR_INVALID_INPUT, &err),
380 )?))
381}
382
383fn stack_value(skip: usize) -> BuiltinResult<Value> {
384 let mut frames = crate::debug_context::current_frames();
385 if frames.is_empty() {
386 if let Some(source) = crate::source_context::current_source_info() {
387 frames.push(crate::debug_context::DebugFrameInfo {
388 function: source.name.to_string(),
389 file: source
390 .fullpath_name
391 .as_ref()
392 .map(ToString::to_string)
393 .unwrap_or_else(|| source.name.to_string()),
394 line: 0,
395 });
396 }
397 }
398 let entries = frames
399 .iter()
400 .skip(skip)
401 .enumerate()
402 .map(|(idx, frame)| stack_struct(frame, idx + 1))
403 .collect::<Vec<_>>();
404 cell_row(entries)
405}
406
407fn parse_dbstack_args(args: &[Value]) -> BuiltinResult<usize> {
408 let mut skip = 0usize;
409 for arg in args {
410 if let Some(text) = text_value(arg) {
411 if text.trim().eq_ignore_ascii_case("-completenames") {
412 continue;
413 }
414 if let Ok(n) = text.trim().parse::<usize>() {
415 skip = n;
416 continue;
417 }
418 return Err(debug_error("dbstack", &DEBUG_ERROR_INVALID_INPUT, text));
419 }
420 if let Some(n) = integer_value(arg) {
421 skip = n;
422 continue;
423 }
424 return Err(debug_error("dbstack", &DEBUG_ERROR_INVALID_INPUT, ""));
425 }
426 Ok(skip)
427}
428
429pub(crate) fn dispatch_dbstack(args: Vec<Value>) -> BuiltinResult<Value> {
430 let skip = parse_dbstack_args(&args)?;
431 let stack = stack_value(skip)?;
432 match crate::output_count::current_output_count() {
433 Some(0) => {
434 record_console_line(ConsoleStream::Stdout, format_stack_for_display(&stack));
435 Ok(empty_value())
436 }
437 Some(n) if n >= 2 => Ok(crate::output_count::output_list_with_padding(
438 n,
439 vec![stack, Value::Num(1.0)],
440 )),
441 _ => Ok(stack),
442 }
443}
444
445fn format_stack_for_display(stack: &Value) -> String {
446 let Value::Cell(cell) = stack else {
447 return String::new();
448 };
449 let lines = cell
450 .data
451 .iter()
452 .filter_map(|value| {
453 let Value::Struct(st) = value else {
454 return None;
455 };
456 let name = st
457 .fields
458 .get("name")
459 .and_then(text_value)
460 .unwrap_or_default();
461 let file = st
462 .fields
463 .get("file")
464 .and_then(text_value)
465 .unwrap_or_default();
466 let line = st
467 .fields
468 .get("line")
469 .and_then(|value| match value {
470 Value::Num(n) => Some(*n as usize),
471 _ => None,
472 })
473 .unwrap_or(0);
474 Some(if file.is_empty() {
475 format!("In {name} at line {line}")
476 } else {
477 format!("In {name} ({file}) at line {line}")
478 })
479 })
480 .collect::<Vec<_>>();
481 lines.join("\n")
482}
483
484fn normalize_lock_key(raw: &str) -> String {
485 let path = Path::new(raw.trim());
486 let name = path
487 .file_stem()
488 .and_then(|stem| stem.to_str())
489 .filter(|stem| !stem.is_empty())
490 .unwrap_or_else(|| raw.trim());
491 name.to_ascii_lowercase()
492}
493
494fn current_lock_key() -> BuiltinResult<String> {
495 if let Some(name) = crate::debug_context::current_function_name() {
496 if !name.is_empty() && name != "<main>" && name != "<anonymous>" {
497 return Ok(normalize_lock_key(&name));
498 }
499 }
500 crate::source_context::current_source_info()
501 .map(|source| normalize_lock_key(&source.name))
502 .filter(|key| !key.is_empty())
503 .ok_or_else(|| debug_error("mlock", &DEBUG_ERROR_NO_CURRENT_FILE, ""))
504}
505
506fn optional_lock_key(args: &[Value], builtin: &'static str) -> BuiltinResult<String> {
507 match args {
508 [] => current_lock_key(),
509 [value] => text_value(value)
510 .map(|text| normalize_lock_key(&text))
511 .filter(|key| !key.is_empty())
512 .ok_or_else(|| debug_error(builtin, &DEBUG_ERROR_INVALID_INPUT, "")),
513 _ => Err(debug_error(builtin, &DEBUG_ERROR_TOO_MANY_INPUTS, "")),
514 }
515}
516
517pub(crate) fn dispatch_mlock(args: Vec<Value>) -> BuiltinResult<Value> {
518 if !args.is_empty() {
519 return Err(debug_error("mlock", &DEBUG_ERROR_TOO_MANY_INPUTS, ""));
520 }
521 let key = current_lock_key()?;
522 LOCKED_FUNCTIONS.with(|locks| {
523 locks.borrow_mut().insert(key);
524 });
525 Ok(empty_value())
526}
527
528pub(crate) fn dispatch_munlock(args: Vec<Value>) -> BuiltinResult<Value> {
529 let key = optional_lock_key(&args, "munlock")?;
530 LOCKED_FUNCTIONS.with(|locks| {
531 locks.borrow_mut().remove(&key);
532 });
533 Ok(empty_value())
534}
535
536pub(crate) fn dispatch_mislocked(args: Vec<Value>) -> BuiltinResult<Value> {
537 let key = optional_lock_key(&args, "mislocked")?;
538 let locked = LOCKED_FUNCTIONS.with(|locks| locks.borrow().contains(&key));
539 Ok(Value::Bool(locked))
540}
541
542fn empty_breakpoint_list() -> BuiltinResult<Value> {
543 cell_row(Vec::new())
544}
545
546pub(crate) fn dispatch_dbstatus(args: Vec<Value>) -> BuiltinResult<Value> {
547 for arg in &args {
548 if text_value(arg).is_none() {
549 return Err(debug_error("dbstatus", &DEBUG_ERROR_INVALID_INPUT, ""));
550 }
551 }
552 let status = empty_breakpoint_list()?;
553 match crate::output_count::current_output_count() {
554 Some(0) => Ok(empty_value()),
555 _ => Ok(status),
556 }
557}
558
559pub(crate) fn dispatch_dbclear(args: Vec<Value>) -> BuiltinResult<Value> {
560 for arg in &args {
561 if text_value(arg).is_none() {
562 return Err(debug_error("dbclear", &DEBUG_ERROR_INVALID_INPUT, ""));
563 }
564 }
565 Ok(empty_value())
566}
567
568fn parse_line_range(value: Option<&Value>) -> BuiltinResult<Option<(usize, usize)>> {
569 let Some(value) = value else {
570 return Ok(None);
571 };
572 let text =
573 text_value(value).ok_or_else(|| debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, ""))?;
574 let Some((start, end)) = text.split_once(':') else {
575 let line = text
576 .trim()
577 .parse::<usize>()
578 .map_err(|_| debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, text.clone()))?;
579 return Ok(Some((line, line)));
580 };
581 let start = start
582 .trim()
583 .parse::<usize>()
584 .map_err(|_| debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, text.clone()))?;
585 let end = end
586 .trim()
587 .parse::<usize>()
588 .map_err(|_| debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, text.clone()))?;
589 if start == 0 || end == 0 || start > end {
590 return Err(debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, text));
591 }
592 Ok(Some((start, end)))
593}
594
595async fn resolve_dbtype_path(name: &str) -> Result<Option<PathBuf>, String> {
596 for candidate in file_candidates(name, &[".m", ""], "dbtype")? {
597 if path_is_file(&candidate).await {
598 return Ok(Some(candidate));
599 }
600 }
601 Ok(None)
602}
603
604pub(crate) async fn dispatch_dbtype(args: Vec<Value>) -> BuiltinResult<Value> {
605 if args.is_empty() {
606 return Err(debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, ""));
607 }
608 if args.len() > 2 {
609 return Err(debug_error("dbtype", &DEBUG_ERROR_TOO_MANY_INPUTS, ""));
610 }
611 let file = text_value(&args[0])
612 .ok_or_else(|| debug_error("dbtype", &DEBUG_ERROR_INVALID_INPUT, "file must be text"))?;
613 let range = parse_line_range(args.get(1))?;
614 let path = resolve_dbtype_path(&file)
615 .await
616 .map_err(|err| debug_error("dbtype", &DEBUG_ERROR_FILE_NOT_FOUND, err))?
617 .ok_or_else(|| debug_error("dbtype", &DEBUG_ERROR_FILE_NOT_FOUND, file.clone()))?;
618 let text = runmat_filesystem::read_to_string_async(&path)
619 .await
620 .map_err(|err| debug_error("dbtype", &DEBUG_ERROR_FILE_READ, err.to_string()))?;
621 let lines = text.lines().collect::<Vec<_>>();
622 let (start, end) = range.unwrap_or((1, lines.len()));
623 let rendered = lines
624 .iter()
625 .enumerate()
626 .filter_map(|(idx, line)| {
627 let line_number = idx + 1;
628 if line_number < start || line_number > end {
629 None
630 } else {
631 Some(format!("{line_number:>5} {line}"))
632 }
633 })
634 .collect::<Vec<_>>()
635 .join("\n");
636 record_console_line(ConsoleStream::Stdout, rendered);
637 Ok(empty_value())
638}
639
640pub(crate) fn dispatch_keyboard(args: Vec<Value>) -> BuiltinResult<Value> {
641 if !args.is_empty() {
642 return Err(debug_error("keyboard", &DEBUG_ERROR_TOO_MANY_INPUTS, ""));
643 }
644 Ok(empty_value())
645}
646
647pub(crate) fn dispatch_getcallinfo(args: Vec<Value>) -> BuiltinResult<Value> {
648 if !args.is_empty() {
649 return Err(debug_error("getcallinfo", &DEBUG_ERROR_TOO_MANY_INPUTS, ""));
650 }
651 let mut info = StructValue::new();
652 let current = crate::debug_context::current_frames()
653 .into_iter()
654 .next()
655 .unwrap_or_else(|| crate::debug_context::DebugFrameInfo {
656 function: String::new(),
657 file: String::new(),
658 line: 0,
659 });
660 info.insert("name", Value::String(current.function));
661 info.insert("file", Value::String(current.file));
662 info.insert("line", Value::Num(current.line as f64));
663 info.insert("stack", stack_value(0)?);
664 Ok(Value::Struct(info))
665}
666
667#[runtime_builtin(
668 name = "dbstack",
669 category = "introspection",
670 summary = "Return or display the active RunMat call stack.",
671 descriptor(self::DBSTACK_DESCRIPTOR),
672 builtin_path = "crate::builtins::introspection::debugging"
673)]
674fn dbstack_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
675 dispatch_dbstack(args)
676}
677
678#[runtime_builtin(
679 name = "dbtype",
680 category = "introspection",
681 summary = "Display MATLAB source text with line numbers.",
682 sink = true,
683 suppress_auto_output = true,
684 descriptor(self::DBTYPE_DESCRIPTOR),
685 builtin_path = "crate::builtins::introspection::debugging"
686)]
687async fn dbtype_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
688 dispatch_dbtype(args).await
689}
690
691#[runtime_builtin(
692 name = "keyboard",
693 category = "introspection",
694 summary = "Compatibility no-op for MATLAB keyboard debugging stops.",
695 sink = true,
696 suppress_auto_output = true,
697 descriptor(self::KEYBOARD_DESCRIPTOR),
698 builtin_path = "crate::builtins::introspection::debugging"
699)]
700fn keyboard_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
701 dispatch_keyboard(args)
702}
703
704#[runtime_builtin(
705 name = "mlock",
706 category = "introspection",
707 summary = "Mark the current function or script as locked in RunMat's compatibility registry.",
708 sink = true,
709 suppress_auto_output = true,
710 descriptor(self::MLOCK_DESCRIPTOR),
711 builtin_path = "crate::builtins::introspection::debugging"
712)]
713fn mlock_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
714 dispatch_mlock(args)
715}
716
717#[runtime_builtin(
718 name = "munlock",
719 category = "introspection",
720 summary = "Remove a function or script from RunMat's lock compatibility registry.",
721 sink = true,
722 suppress_auto_output = true,
723 descriptor(self::MUNLOCK_DESCRIPTOR),
724 builtin_path = "crate::builtins::introspection::debugging"
725)]
726fn munlock_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
727 dispatch_munlock(args)
728}
729
730#[runtime_builtin(
731 name = "mislocked",
732 category = "introspection",
733 summary = "Return whether a function or script is in RunMat's lock compatibility registry.",
734 descriptor(self::MISLOCKED_DESCRIPTOR),
735 builtin_path = "crate::builtins::introspection::debugging"
736)]
737fn mislocked_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
738 dispatch_mislocked(args)
739}
740
741#[runtime_builtin(
742 name = "dbstatus",
743 category = "introspection",
744 summary = "Return RunMat debugger breakpoint status.",
745 descriptor(self::DBSTATUS_DESCRIPTOR),
746 builtin_path = "crate::builtins::introspection::debugging"
747)]
748fn dbstatus_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
749 dispatch_dbstatus(args)
750}
751
752#[runtime_builtin(
753 name = "dbclear",
754 category = "introspection",
755 summary = "Clear RunMat debugger breakpoint status entries.",
756 sink = true,
757 suppress_auto_output = true,
758 descriptor(self::DBCLEAR_DESCRIPTOR),
759 builtin_path = "crate::builtins::introspection::debugging"
760)]
761fn dbclear_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
762 dispatch_dbclear(args)
763}
764
765#[runtime_builtin(
766 name = "getcallinfo",
767 category = "introspection",
768 summary = "Return deterministic information about the current RunMat call context.",
769 descriptor(self::GETCALLINFO_DESCRIPTOR),
770 builtin_path = "crate::builtins::introspection::debugging"
771)]
772fn getcallinfo_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
773 dispatch_getcallinfo(args)
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use runmat_hir::SourceId;
780
781 fn cell_len(value: &Value) -> usize {
782 let Value::Cell(cell) = value else {
783 panic!("expected cell row, got {value:?}");
784 };
785 cell.data.len()
786 }
787
788 #[test]
789 fn dbstack_returns_source_backed_stack_cell() {
790 let source_id = SourceId(31);
791 let _catalog = crate::source_context::replace_source_catalog_with_fullpaths(vec![(
792 source_id,
793 "worker.m".to_string(),
794 Some("/tmp/worker.m".to_string()),
795 "function worker\nx = dbstack;\nend\n".to_string(),
796 )]);
797 let _guard = crate::debug_context::push_frame("worker", Some(source_id), Some((16, 25)));
798 let value = dispatch_dbstack(Vec::new()).expect("dbstack");
799 let Value::Cell(cell) = value else {
800 panic!("expected cell row");
801 };
802 assert_eq!(cell.data.len(), 1);
803 let Value::Struct(st) = &cell.data[0] else {
804 panic!("expected stack struct");
805 };
806 assert_eq!(st.fields.get("name"), Some(&Value::String("worker".into())));
807 assert_eq!(
808 st.fields.get("file"),
809 Some(&Value::String("/tmp/worker.m".into()))
810 );
811 }
812
813 #[test]
814 fn dbstack_skips_leading_entries() {
815 let _outer = crate::debug_context::push_frame("outer", None, None);
816 let _inner = crate::debug_context::push_frame("inner", None, None);
817 let value = dispatch_dbstack(vec![Value::Num(1.0)]).expect("dbstack");
818 assert_eq!(cell_len(&value), 1);
819 let Value::Cell(cell) = value else {
820 unreachable!();
821 };
822 let Value::Struct(st) = &cell.data[0] else {
823 panic!("expected stack struct");
824 };
825 assert_eq!(st.fields.get("name"), Some(&Value::String("outer".into())));
826 }
827
828 #[test]
829 fn mlock_mislocked_munlock_current_function() {
830 let _guard = crate::debug_context::push_frame("locked_fn", None, None);
831 dispatch_mlock(Vec::new()).expect("mlock");
832 assert_eq!(
833 dispatch_mislocked(Vec::new()).expect("mislocked"),
834 Value::Bool(true)
835 );
836 dispatch_munlock(vec![Value::String("locked_fn".into())]).expect("munlock");
837 assert_eq!(
838 dispatch_mislocked(vec![Value::String("locked_fn".into())]).expect("mislocked"),
839 Value::Bool(false)
840 );
841 }
842
843 #[test]
844 fn dbstatus_returns_empty_breakpoint_cell() {
845 let value = dispatch_dbstatus(Vec::new()).expect("dbstatus");
846 assert_eq!(cell_len(&value), 0);
847 }
848
849 #[test]
850 fn dbtype_reads_source_file_with_line_range() {
851 let dir = tempfile::tempdir().expect("tempdir");
852 let path = dir.path().join("demo_dbtype.m");
853 std::fs::write(&path, "a = 1;\nb = 2;\nc = 3;\n").expect("write source");
854 let result = futures::executor::block_on(dispatch_dbtype(vec![
855 Value::String(path.to_string_lossy().to_string()),
856 Value::String("2:3".to_string()),
857 ]))
858 .expect("dbtype");
859 assert!(matches!(result, Value::Tensor(t) if t.data.is_empty()));
860 }
861
862 #[test]
863 fn getcallinfo_reports_current_frame() {
864 let _guard = crate::debug_context::push_frame("callsite", None, None);
865 let value = dispatch_getcallinfo(Vec::new()).expect("getcallinfo");
866 let Value::Struct(st) = value else {
867 panic!("expected struct");
868 };
869 assert_eq!(
870 st.fields.get("name"),
871 Some(&Value::String("callsite".into()))
872 );
873 assert!(matches!(st.fields.get("stack"), Some(Value::Cell(_))));
874 }
875}