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