1use runmat_builtins::{
4 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
5 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
6};
7use runmat_builtins::{
8 BuiltinExtensionDescriptor, BuiltinExtensionMode, BuiltinIntegerAuditDescriptor,
9 BuiltinIntegerAuditKind,
10};
11use runmat_macros::runtime_builtin;
12use runmat_value::{CharArray, LogicalArray, Tensor, Value};
13
14use crate::builtins::common::spec::{
15 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
16 ReductionNaN, ResidencyPolicy, ShapeRequirements,
17};
18use crate::interaction;
19use crate::{
20 build_runtime_error, call_builtin_async, gather_if_needed_async, BuiltinResult, RuntimeError,
21};
22
23const DEFAULT_PROMPT: &str = "Input: ";
24const BUILTIN_NAME: &str = "input";
25
26const INPUT_NO_PROMPT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
27 id: "input-no-prompt",
28 mode: BuiltinExtensionMode::RunMatOnly,
29 description: "input() with RunMat's default prompt is a RunMat extension",
30 error_identifier: Some("RunMat:compatibility:InputNoPromptExtension"),
31};
32const INPUT_SWAPPED_ARGUMENTS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
33 id: "input-swapped-arguments",
34 mode: BuiltinExtensionMode::RunMatOnly,
35 description: "input('s', prompt) argument order is a RunMat extension",
36 error_identifier: Some("RunMat:compatibility:InputSwappedArgumentsExtension"),
37};
38pub const INPUT_EXTENSIONS: [BuiltinExtensionDescriptor; 2] =
39 [INPUT_NO_PROMPT_EXTENSION, INPUT_SWAPPED_ARGUMENTS_EXTENSION];
40pub const INPUT_INTEGER_AUDIT: BuiltinIntegerAuditDescriptor = BuiltinIntegerAuditDescriptor {
41 kind: BuiltinIntegerAuditKind::NotApplicable,
42 canonical_builtin: None,
43 notes: "input accepts only a text prompt and the text flag 's'. In expression mode the entered expression is evaluated normally, so any integer result retains the class and shape produced by that expression rather than serving as an integer input role of input itself.",
44};
45
46const INPUT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
47 name: "value",
48 ty: BuiltinParamType::Any,
49 arity: BuiltinParamArity::Required,
50 default: None,
51 description: "Parsed scalar/matrix value, or raw text when using string mode.",
52}];
53const INPUT_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
54const INPUT_INPUTS_PROMPT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
55 name: "prompt",
56 ty: BuiltinParamType::Any,
57 arity: BuiltinParamArity::Required,
58 default: None,
59 description: "Prompt text shown to the user.",
60}];
61const INPUT_INPUTS_PROMPT_FLAG: [BuiltinParamDescriptor; 2] = [
62 BuiltinParamDescriptor {
63 name: "prompt",
64 ty: BuiltinParamType::Any,
65 arity: BuiltinParamArity::Required,
66 default: None,
67 description: "Prompt text shown to the user.",
68 },
69 BuiltinParamDescriptor {
70 name: "stringFlag",
71 ty: BuiltinParamType::StringScalar,
72 arity: BuiltinParamArity::Required,
73 default: None,
74 description: "Set to 's' to return the raw input text.",
75 },
76];
77const INPUT_INPUTS_FLAG_PROMPT: [BuiltinParamDescriptor; 2] = [
78 BuiltinParamDescriptor {
79 name: "stringFlag",
80 ty: BuiltinParamType::StringScalar,
81 arity: BuiltinParamArity::Required,
82 default: None,
83 description: "Set to 's' to return the raw input text.",
84 },
85 BuiltinParamDescriptor {
86 name: "prompt",
87 ty: BuiltinParamType::Any,
88 arity: BuiltinParamArity::Required,
89 default: None,
90 description: "Prompt text shown to the user.",
91 },
92];
93const INPUT_SIGNATURES: [BuiltinSignatureDescriptor; 4] = [
94 BuiltinSignatureDescriptor {
95 label: "value = input()",
96 inputs: &INPUT_INPUTS_NONE,
97 outputs: &INPUT_OUTPUT,
98 },
99 BuiltinSignatureDescriptor {
100 label: "value = input(prompt)",
101 inputs: &INPUT_INPUTS_PROMPT,
102 outputs: &INPUT_OUTPUT,
103 },
104 BuiltinSignatureDescriptor {
105 label: "value = input(prompt, stringFlag)",
106 inputs: &INPUT_INPUTS_PROMPT_FLAG,
107 outputs: &INPUT_OUTPUT,
108 },
109 BuiltinSignatureDescriptor {
110 label: "value = input(stringFlag, prompt)",
111 inputs: &INPUT_INPUTS_FLAG_PROMPT,
112 outputs: &INPUT_OUTPUT,
113 },
114];
115const INPUT_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
116 code: "RM.INPUT.TOO_MANY_INPUTS",
117 identifier: Some("RunMat:input:TooManyInputs"),
118 when: "More than two input arguments are passed to input.",
119 message: "input: too many inputs",
120};
121const INPUT_ERROR_INVALID_STRING_FLAG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
122 code: "RM.INPUT.INVALID_STRING_FLAG",
123 identifier: Some("RunMat:input:InvalidStringFlag"),
124 when: "The string mode flag is not a scalar string/char 's'.",
125 message: "input: invalid string flag",
126};
127const INPUT_ERROR_PROMPT_ROW_VECTOR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
128 code: "RM.INPUT.PROMPT_ROW_VECTOR",
129 identifier: Some("RunMat:input:PromptMustBeRowVector"),
130 when: "Prompt char array is not 1-by-N.",
131 message: "input: prompt must be a row vector",
132};
133const INPUT_ERROR_PROMPT_SCALAR_STRING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
134 code: "RM.INPUT.PROMPT_SCALAR_STRING",
135 identifier: Some("RunMat:input:PromptMustBeScalarString"),
136 when: "Prompt string array is not scalar.",
137 message: "input: prompt must be a scalar string",
138};
139const INPUT_ERROR_INVALID_PROMPT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
140 code: "RM.INPUT.INVALID_PROMPT_TYPE",
141 identifier: Some("RunMat:input:InvalidPromptType"),
142 when: "Prompt is not a string scalar or row char vector.",
143 message: "input: invalid prompt type",
144};
145const INPUT_ERROR_INTERACTION_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
146 code: "RM.INPUT.INTERACTION_FAILED",
147 identifier: Some("RunMat:input:InteractionFailed"),
148 when: "Interactive prompt callback fails.",
149 message: "input: interaction failed",
150};
151const INPUT_ERROR_EVAL_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
152 code: "RM.INPUT.EVAL_FAILED",
153 identifier: Some("RunMat:input:EvalFailed"),
154 when: "Expression evaluation hook rejects the input expression.",
155 message: "input: invalid expression",
156};
157const INPUT_ERROR_INVALID_NUMERIC_EXPRESSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
158 code: "RM.INPUT.INVALID_NUMERIC_EXPRESSION",
159 identifier: Some("RunMat:input:InvalidNumericExpression"),
160 when: "Numeric fallback parser rejects the input expression.",
161 message: "input: invalid numeric expression",
162};
163const INPUT_ERRORS: [BuiltinErrorDescriptor; 8] = [
164 INPUT_ERROR_TOO_MANY_INPUTS,
165 INPUT_ERROR_INVALID_STRING_FLAG,
166 INPUT_ERROR_PROMPT_ROW_VECTOR,
167 INPUT_ERROR_PROMPT_SCALAR_STRING,
168 INPUT_ERROR_INVALID_PROMPT_TYPE,
169 INPUT_ERROR_INTERACTION_FAILED,
170 INPUT_ERROR_EVAL_FAILED,
171 INPUT_ERROR_INVALID_NUMERIC_EXPRESSION,
172];
173pub const INPUT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
174 signatures: &INPUT_SIGNATURES,
175 output_mode: BuiltinOutputMode::Fixed,
176 completion_policy: BuiltinCompletionPolicy::Public,
177 errors: &INPUT_ERRORS,
178};
179
180fn input_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
181 input_error_with(error, error.message)
182}
183
184fn input_error_with(
185 error: &'static BuiltinErrorDescriptor,
186 message: impl Into<String>,
187) -> RuntimeError {
188 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
189 if let Some(identifier) = error.identifier {
190 builder = builder.with_identifier(identifier.to_string());
191 }
192 builder.build()
193}
194
195fn input_error_with_source(
196 error: &'static BuiltinErrorDescriptor,
197 message: impl Into<String>,
198 source: RuntimeError,
199) -> RuntimeError {
200 let mut builder = build_runtime_error(message)
201 .with_builtin(BUILTIN_NAME)
202 .with_source(source);
203 if let Some(identifier) = error.identifier {
204 builder = builder.with_identifier(identifier.to_string());
205 }
206 builder.build()
207}
208
209#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::input")]
210pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
211 name: "input",
212 op_kind: GpuOpKind::Custom("interaction"),
213 supported_precisions: &[],
214 broadcast: BroadcastSemantics::None,
215 provider_hooks: &[],
216 constant_strategy: ConstantStrategy::InlineLiteral,
217 residency: ResidencyPolicy::GatherImmediately,
218 nan_mode: ReductionNaN::Include,
219 two_pass_threshold: None,
220 workgroup_size: None,
221 accepts_nan_mode: false,
222 notes: "Prompts execute on the host. Interactive resident prompt and flag values are rejected before provider access; expression results retain the residency established by the entered expression itself.",
223};
224
225#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::input")]
226pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
227 name: "input",
228 shape: ShapeRequirements::Any,
229 constant_strategy: ConstantStrategy::InlineLiteral,
230 elementwise: None,
231 reduction: None,
232 emits_nan: false,
233 notes: "Side-effecting builtin; excluded from fusion plans.",
234};
235
236#[runtime_builtin(
237 name = "input",
238 summary = "Prompt users for interactive input.",
239 extensions(INPUT_EXTENSIONS),
240 integer_audit(crate::builtins::io::input::INPUT_INTEGER_AUDIT),
241 type_resolver(crate::builtins::io::type_resolvers::input_type),
242 descriptor(crate::builtins::io::input::INPUT_DESCRIPTOR),
243 builtin_path = "crate::builtins::io::input"
244)]
245async fn input_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
246 if args.len() > 2 {
247 return Err(input_error(&INPUT_ERROR_TOO_MANY_INPUTS));
248 }
249
250 if args.is_empty() {
251 crate::compatibility::ensure_builtin_extension_enabled(
252 &INPUT_NO_PROMPT_EXTENSION,
253 BUILTIN_NAME,
254 )?;
255 }
256 if args.iter().any(crate::dispatcher::value_contains_gpu) {
257 return Err(input_error(&INPUT_ERROR_INVALID_PROMPT_TYPE));
258 }
259
260 let mut prompt_index = if args.is_empty() { None } else { Some(0usize) };
261 let mut parsed_flag: Option<bool> = None;
262
263 if let Some(idx) = if args.len() == 2 { Some(1usize) } else { None } {
264 match parse_string_flag(&args[idx]).await {
265 Ok(flag) => parsed_flag = Some(flag),
266 Err(original_err) => {
267 if let Some(prompt_idx) = prompt_index {
268 match parse_string_flag(&args[prompt_idx]).await {
269 Ok(swapped_flag) => {
270 crate::compatibility::ensure_builtin_extension_enabled(
271 &INPUT_SWAPPED_ARGUMENTS_EXTENSION,
272 BUILTIN_NAME,
273 )?;
274 parsed_flag = Some(swapped_flag);
275 prompt_index = Some(idx);
276 }
277 Err(_) => {
278 return Err(original_err);
279 }
280 }
281 } else {
282 return Err(original_err);
283 }
284 }
285 }
286 }
287
288 let prompt = if let Some(idx) = prompt_index {
289 parse_prompt(&args[idx]).await?
290 } else {
291 DEFAULT_PROMPT.to_string()
292 };
293 let return_string = parsed_flag.unwrap_or(false);
294 loop {
295 let line = interaction::request_line_async(&prompt, true)
296 .await
297 .map_err(|err| {
298 let message = err.message().to_string();
299 input_error_with_source(
300 &INPUT_ERROR_INTERACTION_FAILED,
301 format!("input: {message}"),
302 err,
303 )
304 })?;
305 if return_string {
306 return Ok(Value::CharArray(CharArray::new_row(&line)));
307 }
308 if let Ok(value) = parse_numeric_response(&line).await {
309 return Ok(value);
310 }
311 }
312}
313
314async fn parse_prompt(value: &Value) -> Result<String, RuntimeError> {
315 let gathered = gather_if_needed_async(value).await?;
316 match gathered {
317 Value::CharArray(ca) => {
318 if ca.rows != 1 {
319 Err(input_error(&INPUT_ERROR_PROMPT_ROW_VECTOR))
320 } else {
321 Ok(ca.data.iter().collect())
322 }
323 }
324 Value::String(text) => Ok(text),
325 Value::StringArray(sa) => {
326 if sa.data.len() == 1 {
327 Ok(sa.data[0].clone())
328 } else {
329 Err(input_error(&INPUT_ERROR_PROMPT_SCALAR_STRING))
330 }
331 }
332 other => Err(input_error_with(
333 &INPUT_ERROR_INVALID_PROMPT_TYPE,
334 format!("input: invalid prompt type ({other:?})"),
335 )),
336 }
337}
338
339async fn parse_string_flag(value: &Value) -> Result<bool, RuntimeError> {
340 let gathered = gather_if_needed_async(value).await?;
341 let text = match gathered {
342 Value::CharArray(ca) if ca.rows == 1 => ca.data.iter().collect::<String>(),
343 Value::String(s) => s,
344 Value::StringArray(sa) if sa.data.len() == 1 => sa.data[0].clone(),
345 other => {
346 return Err(input_error_with(
347 &INPUT_ERROR_INVALID_STRING_FLAG,
348 format!("input: invalid string flag ({other:?})"),
349 ))
350 }
351 };
352 if text == "s" {
353 Ok(true)
354 } else {
355 Err(input_error_with(
356 &INPUT_ERROR_INVALID_STRING_FLAG,
357 format!("input: invalid string flag ({text})"),
358 ))
359 }
360}
361
362async fn parse_numeric_response(line: &str) -> Result<Value, RuntimeError> {
363 let trimmed = line.trim();
364 if trimmed.is_empty() || trimmed == "[]" {
365 return Ok(Value::Tensor(Tensor::zeros(vec![0, 0])));
366 }
367
368 if let Some(v) = parse_scalar_value(trimmed) {
371 return Ok(v);
372 }
373
374 if trimmed.starts_with('[') && trimmed.ends_with(']') {
377 if let Some(v) = parse_matrix_literal(trimmed) {
378 return Ok(v);
379 }
380 }
381
382 if let Some(hook) = interaction::current_eval_hook() {
387 return hook(trimmed.to_string()).await.map_err(|err| {
388 let message = err.message().to_string();
389 input_error_with_source(
390 &INPUT_ERROR_EVAL_FAILED,
391 format!("input: invalid expression ({message})"),
392 err,
393 )
394 });
395 }
396
397 let parsed = call_builtin_async("str2double", &[Value::String(trimmed.to_string())])
399 .await
400 .map_err(|err| {
401 let message = err.message().to_string();
402 input_error_with_source(
403 &INPUT_ERROR_INVALID_NUMERIC_EXPRESSION,
404 format!("input: invalid numeric expression ({message})"),
405 err,
406 )
407 })?;
408 if matches!(parsed, Value::Num(value) if value.is_nan()) {
409 return Err(input_error_with(
410 &INPUT_ERROR_INVALID_NUMERIC_EXPRESSION,
411 INPUT_ERROR_INVALID_NUMERIC_EXPRESSION.message,
412 ));
413 }
414 Ok(parsed)
415}
416
417fn parse_scalar_value(s: &str) -> Option<Value> {
429 match s.to_ascii_lowercase().as_str() {
430 "true" => return Some(Value::Bool(true)),
431 "false" => return Some(Value::Bool(false)),
432 "pi" => return Some(Value::Num(std::f64::consts::PI)),
433 "inf" | "+inf" | "infinity" | "+infinity" => return Some(Value::Num(f64::INFINITY)),
434 "-inf" | "-infinity" => return Some(Value::Num(f64::NEG_INFINITY)),
435 "nan" => return Some(Value::Num(f64::NAN)),
436 _ => {}
437 }
438 let has_non_numeric = s.chars().any(|c| {
442 matches!(c, '[' | ']' | ',' | ';' | '(' | ')' | ' ' | '\t')
443 || (c.is_ascii_alphabetic() && c != 'e' && c != 'E' && c != 'i' && c != 'j')
444 });
445 if has_non_numeric {
446 return None;
447 }
448 s.parse::<f64>().ok().map(Value::Num)
449}
450
451fn parse_matrix_literal(s: &str) -> Option<Value> {
461 let inner = s.strip_prefix('[')?.strip_suffix(']')?;
462 let inner = inner.trim();
463 if inner.is_empty() {
464 return Some(Value::Tensor(Tensor::zeros(vec![0, 0])));
465 }
466
467 let row_strs: Vec<&str> = inner.split(';').collect();
468 let mut values: Vec<Value> = Vec::new();
469 let mut nrows = 0usize;
470 let mut ncols: Option<usize> = None;
471
472 for row_str in &row_strs {
473 let tokens: Vec<&str> = row_str
474 .split(|c: char| c == ',' || c.is_ascii_whitespace())
475 .filter(|t| !t.is_empty())
476 .collect();
477 if tokens.is_empty() {
478 continue;
479 }
480 match ncols {
481 None => ncols = Some(tokens.len()),
482 Some(expected) if tokens.len() != expected => return None,
483 _ => {}
484 }
485 for token in &tokens {
486 values.push(parse_scalar_value(token)?);
487 }
488 nrows += 1;
489 }
490
491 let ncols = ncols.unwrap_or(0);
492 if nrows == 0 || ncols == 0 {
493 return Some(Value::Tensor(Tensor::zeros(vec![0, 0])));
494 }
495 if nrows == 1 && ncols == 1 {
497 return Some(values.remove(0));
498 }
499
500 let all_logical = values.iter().all(|v| matches!(v, Value::Bool(_)));
505 if all_logical {
506 let mut data: Vec<u8> = vec![0u8; nrows * ncols];
507 for r in 0..nrows {
508 for c in 0..ncols {
509 let row_major_idx = r * ncols + c;
510 let col_major_idx = r + c * nrows;
511 data[col_major_idx] = match &values[row_major_idx] {
512 Value::Bool(b) => u8::from(*b),
513 _ => unreachable!(),
514 };
515 }
516 }
517 LogicalArray::new(data, vec![nrows, ncols])
518 .ok()
519 .map(Value::LogicalArray)
520 } else {
521 let mut data: Vec<f64> = vec![0f64; nrows * ncols];
522 for r in 0..nrows {
523 for c in 0..ncols {
524 let row_major_idx = r * ncols + c;
525 let col_major_idx = r + c * nrows;
526 data[col_major_idx] = match &values[row_major_idx] {
527 Value::Num(f) => *f,
528 Value::Bool(b) => f64::from(u8::from(*b)),
529 _ => unreachable!(),
530 };
531 }
532 }
533 Tensor::new_2d(data, nrows, ncols).ok().map(Value::Tensor)
534 }
535}
536
537#[cfg(test)]
538pub(crate) mod tests {
539 use super::*;
540 use crate::interaction::{push_queued_response, InteractionResponse};
541
542 fn input_with_empty_prompt() -> BuiltinResult<Value> {
543 futures::executor::block_on(input_builtin(vec![Value::String(String::new())]))
544 }
545
546 #[test]
547 fn input_descriptor_signatures_cover_core_forms() {
548 let labels: Vec<&str> = INPUT_DESCRIPTOR
549 .signatures
550 .iter()
551 .map(|sig| sig.label)
552 .collect();
553 assert!(labels.contains(&"value = input()"));
554 assert!(labels.contains(&"value = input(prompt)"));
555 assert!(labels.contains(&"value = input(prompt, stringFlag)"));
556 assert!(labels.contains(&"value = input(stringFlag, prompt)"));
557 }
558
559 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
560 #[test]
561 fn numeric_input_parses_scalar() {
562 push_queued_response(Ok(InteractionResponse::Line("41".into())));
563 let value = input_with_empty_prompt().expect("input");
564 assert_eq!(value, Value::Num(41.0));
565 }
566
567 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
568 #[test]
569 fn string_mode_returns_char_row() {
570 push_queued_response(Ok(InteractionResponse::Line("RunMat".into())));
571 let prompt = Value::CharArray(CharArray::new_row("Name: "));
572 let mode = Value::String("s".to_string());
573 let value = futures::executor::block_on(input_builtin(vec![prompt, mode])).expect("input");
574 assert_eq!(value, Value::CharArray(CharArray::new_row("RunMat")));
575 }
576
577 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
578 #[test]
579 fn empty_response_returns_empty_tensor() {
580 push_queued_response(Ok(InteractionResponse::Line(" ".into())));
581 let value = input_with_empty_prompt().expect("input");
582 match value {
583 Value::Tensor(t) => assert!(t.materialize_f64().is_empty()),
584 other => panic!("expected empty tensor, got {other:?}"),
585 }
586 }
587
588 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
589 #[test]
590 fn matrix_literal_parses_without_eval_hook() {
591 push_queued_response(Ok(InteractionResponse::Line("[1 2 3]".into())));
594 let value = input_with_empty_prompt().expect("input");
595 match value {
596 Value::Tensor(t) => {
597 assert_eq!(t.rows, 1);
598 assert_eq!(t.cols, 3);
599 assert_eq!(t.materialize_f64(), vec![1.0, 2.0, 3.0]);
600 }
601 other => panic!("expected 1×3 tensor, got {other:?}"),
602 }
603 }
604
605 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
606 #[test]
607 fn named_constants_parse_without_eval_hook() {
608 push_queued_response(Ok(InteractionResponse::Line("pi".into())));
609 let value = input_with_empty_prompt().expect("input");
610 assert_eq!(value, Value::Num(std::f64::consts::PI));
611 }
612
613 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
617 #[test]
618 fn bare_e_is_not_eulers_number() {
619 assert_eq!(parse_scalar_value("e"), None);
620 assert_eq!(parse_scalar_value("E"), None);
621 }
622
623 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
625 #[test]
626 fn matrix_with_bare_e_does_not_parse() {
627 assert_eq!(parse_matrix_literal("[1 e 3]"), None);
628 }
629
630 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
631 #[test]
632 fn true_input_returns_logical_not_double() {
633 push_queued_response(Ok(InteractionResponse::Line("true".into())));
634 let value = input_with_empty_prompt().expect("input");
635 assert_eq!(value, Value::Bool(true));
636 }
637
638 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
639 #[test]
640 fn false_input_returns_logical_not_double() {
641 push_queued_response(Ok(InteractionResponse::Line("false".into())));
642 let value = input_with_empty_prompt().expect("input");
643 assert_eq!(value, Value::Bool(false));
644 }
645
646 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
647 #[test]
648 fn bool_input_is_case_insensitive() {
649 push_queued_response(Ok(InteractionResponse::Line("TRUE".into())));
650 let value = input_with_empty_prompt().expect("input");
651 assert_eq!(value, Value::Bool(true));
652 }
653
654 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
655 #[test]
656 fn column_vector_parses_without_eval_hook() {
657 push_queued_response(Ok(InteractionResponse::Line("[1;2;3]".into())));
658 let value = input_with_empty_prompt().expect("input");
659 match value {
660 Value::Tensor(t) => {
661 assert_eq!(t.rows, 3);
662 assert_eq!(t.cols, 1);
663 assert_eq!(t.materialize_f64(), vec![1.0, 2.0, 3.0]);
664 }
665 other => panic!("expected 3×1 tensor, got {other:?}"),
666 }
667 }
668
669 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
670 #[test]
671 fn logical_row_vector_parses_as_logical_array() {
672 push_queued_response(Ok(InteractionResponse::Line("[true false]".into())));
673 let value = input_with_empty_prompt().expect("input");
674 match value {
675 Value::LogicalArray(la) => {
676 assert_eq!(la.shape, vec![1, 2]);
677 assert_eq!(la.data, vec![1, 0]);
678 }
679 other => panic!("expected LogicalArray, got {other:?}"),
680 }
681 }
682
683 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
684 #[test]
685 fn logical_column_vector_parses_as_logical_array() {
686 push_queued_response(Ok(InteractionResponse::Line("[true; false]".into())));
687 let value = input_with_empty_prompt().expect("input");
688 match value {
689 Value::LogicalArray(la) => {
690 assert_eq!(la.shape, vec![2, 1]);
691 assert_eq!(la.data, vec![1, 0]);
692 }
693 other => panic!("expected LogicalArray, got {other:?}"),
694 }
695 }
696
697 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
698 #[test]
699 fn mixed_logical_and_numeric_coerces_to_double_tensor() {
700 push_queued_response(Ok(InteractionResponse::Line("[true 2.0]".into())));
701 let value = input_with_empty_prompt().expect("input");
702 match value {
703 Value::Tensor(t) => {
704 assert_eq!(t.rows, 1);
705 assert_eq!(t.cols, 2);
706 assert_eq!(t.materialize_f64(), vec![1.0, 2.0]);
707 }
708 other => panic!("expected Tensor, got {other:?}"),
709 }
710 }
711
712 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
713 #[test]
714 fn matrix_2x2_column_major_layout() {
715 push_queued_response(Ok(InteractionResponse::Line("[1 2; 3 4]".into())));
718 let value = input_with_empty_prompt().expect("input");
719 match value {
720 Value::Tensor(t) => {
721 assert_eq!(t.rows, 2);
722 assert_eq!(t.cols, 2);
723 assert_eq!(t.get2(0, 0).unwrap(), 1.0, "(0,0) should be 1");
724 assert_eq!(t.get2(0, 1).unwrap(), 2.0, "(0,1) should be 2");
725 assert_eq!(t.get2(1, 0).unwrap(), 3.0, "(1,0) should be 3");
726 assert_eq!(t.get2(1, 1).unwrap(), 4.0, "(1,1) should be 4");
727 }
728 other => panic!("expected 2×2 tensor, got {other:?}"),
729 }
730 }
731
732 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
733 #[test]
734 fn logical_matrix_2x2_column_major_layout() {
735 push_queued_response(Ok(InteractionResponse::Line(
737 "[true false; false true]".into(),
738 )));
739 let value = input_with_empty_prompt().expect("input");
740 match value {
741 Value::LogicalArray(la) => {
742 assert_eq!(la.shape, vec![2, 2]);
743 assert_eq!(la.data, vec![1, 0, 0, 1]);
745 }
746 other => panic!("expected 2×2 LogicalArray, got {other:?}"),
747 }
748 }
749
750 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
751 #[test]
752 fn invalid_string_flag_errors_before_prompt() {
753 push_queued_response(Ok(InteractionResponse::Line("ignored".into())));
754 let prompt = Value::String("Ready?".to_string());
755 let bad_flag = Value::String("not-string-mode".to_string());
756 let err = futures::executor::block_on(input_builtin(vec![prompt, bad_flag])).unwrap_err();
757 assert_eq!(err.identifier(), Some("RunMat:input:InvalidStringFlag"));
758 }
759
760 #[test]
761 fn strict_mode_gates_runmat_only_call_forms_before_interaction() {
762 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
763 let no_prompt = futures::executor::block_on(input_builtin(vec![])).unwrap_err();
764 assert_eq!(
765 no_prompt.identifier(),
766 INPUT_NO_PROMPT_EXTENSION.error_identifier
767 );
768 let swapped = futures::executor::block_on(input_builtin(vec![
769 Value::String("s".into()),
770 Value::String("Prompt: ".into()),
771 ]))
772 .unwrap_err();
773 assert_eq!(
774 swapped.identifier(),
775 INPUT_SWAPPED_ARGUMENTS_EXTENSION.error_identifier
776 );
777 }
778
779 #[test]
780 fn resident_prompt_rejects_without_provider_access() {
781 let resident = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
782 shape: vec![1, 1],
783 device_id: u32::MAX,
784 buffer_id: u64::MAX,
785 descriptor: Default::default(),
786 });
787 let error = futures::executor::block_on(input_builtin(vec![resident])).unwrap_err();
788 assert_eq!(
789 error.identifier(),
790 INPUT_ERROR_INVALID_PROMPT_TYPE.identifier
791 );
792 }
793
794 #[test]
795 fn input_integer_audit_is_explicitly_not_applicable() {
796 assert_eq!(
797 INPUT_INTEGER_AUDIT.kind,
798 BuiltinIntegerAuditKind::NotApplicable
799 );
800 assert_eq!(INPUT_EXTENSIONS.len(), 2);
801 }
802}