1pub mod errors;
8pub mod parse;
9pub mod repl;
10
11pub use errors::{ConsoleError, Result};
12pub use parse::{parse_line, ReplCommand};
13pub use repl::run_repl;
14
15pub fn f30() -> i32 {
24 if !matches!(parse_line("quit"), ReplCommand::Quit) {
26 return 1;
27 }
28
29 if !matches!(parse_line(""), ReplCommand::Empty) {
31 return 2;
32 }
33
34 if !matches!(parse_line("# comment"), ReplCommand::Comment) {
36 return 3;
37 }
38
39 match parse_line("debug msg=hello") {
41 ReplCommand::Invoke { module, args } => {
42 if module != "debug" {
43 return 4;
44 }
45 match args.get("msg").and_then(|v| v.as_str()) {
46 Some("hello") => {}
47 _ => return 5,
48 }
49 }
50 _ => return 6,
51 }
52
53 if !matches!(parse_line("EXIT"), ReplCommand::Quit) {
55 return 7;
56 }
57
58 let completer = repl::ConsoleCompleter::from_builtins();
61 let (start, candidates) = completer.complete_word("deb", 3);
62 if start != 0 {
63 return 8;
64 }
65 if !candidates.iter().any(|c| c == "debug") {
66 return 9;
67 }
68
69 let (_, all) = completer.complete_word("", 0);
71 if !all.iter().any(|c| c == "ping" || c == "runsible_builtin.ping") {
72 return 10;
73 }
74
75 let (_, none) = completer.complete_word("xyz_no_such_prefix_zzz", 22);
77 if !none.is_empty() {
78 return 11;
79 }
80
81 0
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn parse_quit() {
90 assert!(matches!(parse_line("quit"), ReplCommand::Quit));
91 assert!(matches!(parse_line("exit"), ReplCommand::Quit));
92 assert!(matches!(parse_line("EXIT"), ReplCommand::Quit));
93 assert!(matches!(parse_line(" quit "), ReplCommand::Quit));
94 assert!(matches!(parse_line("Quit"), ReplCommand::Quit));
95 }
96
97 #[test]
98 fn parse_empty() {
99 assert!(matches!(parse_line(""), ReplCommand::Empty));
100 assert!(matches!(parse_line(" "), ReplCommand::Empty));
101 assert!(matches!(parse_line("\t\t"), ReplCommand::Empty));
102 }
103
104 #[test]
105 fn parse_comment() {
106 assert!(matches!(parse_line("# hello"), ReplCommand::Comment));
107 assert!(matches!(parse_line("#no space"), ReplCommand::Comment));
108 assert!(matches!(parse_line(" # leading whitespace"), ReplCommand::Comment));
109 }
110
111 #[test]
112 fn parse_invoke_no_args() {
113 match parse_line("runsible_builtin.ping") {
114 ReplCommand::Invoke { module, args } => {
115 assert_eq!(module, "runsible_builtin.ping");
116 let table = args.as_table().expect("args must be a table");
117 assert!(table.is_empty(), "expected empty args, got {table:?}");
118 }
119 other => panic!("expected Invoke, got {other:?}"),
120 }
121 }
122
123 #[test]
124 fn parse_invoke_kv_args() {
125 match parse_line("runsible_builtin.debug msg=hello") {
126 ReplCommand::Invoke { module, args } => {
127 assert_eq!(module, "runsible_builtin.debug");
128 assert_eq!(
129 args.get("msg").and_then(|v| v.as_str()),
130 Some("hello")
131 );
132 }
133 other => panic!("expected Invoke, got {other:?}"),
134 }
135 }
136
137 #[test]
138 fn parse_invoke_multi_kv() {
139 match parse_line("debug msg=hi var=x") {
140 ReplCommand::Invoke { module, args } => {
141 assert_eq!(module, "debug");
142 assert_eq!(args.get("msg").and_then(|v| v.as_str()), Some("hi"));
143 assert_eq!(args.get("var").and_then(|v| v.as_str()), Some("x"));
144 }
145 other => panic!("expected Invoke, got {other:?}"),
146 }
147 }
148
149 #[test]
150 fn parse_unknown() {
151 match parse_line("garbage with spaces") {
155 ReplCommand::Invoke { module, args } => {
156 assert_eq!(module, "garbage");
157 let table = args.as_table().expect("args must be a table");
158 assert!(table.is_empty(), "non-kv tokens should be skipped");
159 }
160 other => panic!("expected Invoke for non-quit input, got {other:?}"),
161 }
162 }
163
164 #[test]
169 fn parse_colon_quit_is_unrecognized_invoke() {
170 match parse_line(":quit") {
171 ReplCommand::Invoke { module, args } => {
172 assert_eq!(module, ":quit");
173 let table = args.as_table().expect("args must be a table");
174 assert!(table.is_empty());
175 }
176 other => panic!("expected Invoke (current M0 behavior), got {other:?}"),
177 }
178 }
179
180 #[test]
183 fn parse_alias_only_invoke_has_empty_args() {
184 match parse_line("debug") {
185 ReplCommand::Invoke { module, args } => {
186 assert_eq!(module, "debug");
187 let table = args.as_table().expect("args must be a table");
188 assert!(table.is_empty(), "expected no args; got {table:?}");
189 }
190 other => panic!("expected Invoke, got {other:?}"),
191 }
192 }
193
194 #[test]
198 fn arg_with_spaces_not_supported_yet() {
199 match parse_line("debug msg=\"hello world\"") {
200 ReplCommand::Invoke { module, args } => {
201 assert_eq!(module, "debug");
202 let v = args.get("msg").and_then(|v| v.as_str()).unwrap_or("");
206 assert!(
207 v.contains("hello") && v.contains('"'),
208 "expected the partial-quote value to land in `msg`; got: {v:?}"
209 );
210 assert!(
211 !v.contains("world"),
212 "M0 tokenizer must NOT join whitespace-separated quoted args; got: {v:?}"
213 );
214 }
215 other => panic!("expected Invoke, got {other:?}"),
216 }
217 }
218
219 #[test]
221 fn parse_duplicate_keys_last_wins() {
222 match parse_line("debug msg=hello msg=overwritten") {
223 ReplCommand::Invoke { module, args } => {
224 assert_eq!(module, "debug");
225 assert_eq!(
226 args.get("msg").and_then(|v| v.as_str()),
227 Some("overwritten"),
228 "duplicate keys: last value must win"
229 );
230 }
231 other => panic!("expected Invoke, got {other:?}"),
232 }
233 }
234
235 #[test]
238 fn parse_malformed_token_empty_key_inserted() {
239 match parse_line("debug =bare") {
240 ReplCommand::Invoke { module, args } => {
241 assert_eq!(module, "debug");
242 let table = args.as_table().expect("args must be a table");
243 assert_eq!(
246 table.get("").and_then(|v| v.as_str()),
247 Some("bare"),
248 "M0 inserts empty-key tokens; got: {table:?}"
249 );
250 }
251 other => panic!("expected Invoke, got {other:?}"),
252 }
253 }
254
255 #[test]
257 fn parse_comment_with_reserved_word_in_body() {
258 assert!(matches!(
259 parse_line("# this is a comment with debug in it"),
260 ReplCommand::Comment
261 ));
262 assert!(matches!(
264 parse_line(" # padded"),
265 ReplCommand::Comment
266 ));
267 }
268
269 #[test]
271 fn parse_tabs_and_spaces_only_is_empty() {
272 assert!(matches!(parse_line("\t "), ReplCommand::Empty));
273 assert!(matches!(parse_line(" \t \t "), ReplCommand::Empty));
274 }
275
276 #[test]
278 fn parse_uppercase_exit_is_quit() {
279 assert!(matches!(parse_line("EXIT"), ReplCommand::Quit));
280 assert!(matches!(parse_line("ExIt"), ReplCommand::Quit));
281 assert!(matches!(parse_line("QUIT"), ReplCommand::Quit));
282 }
283}