unilang_parser 0.39.1

Parser for Unilang CLI instruction syntax.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! ## Test Matrix for Comprehensive Parsing
//!
//! This matrix details a comprehensive set of test cases for the Unilang instruction parser,
//! covering various instruction structures, command path formats, argument types, parser options,
//! and error conditions.
//!
//! **Test Factors: **
//! - Instruction Structure: Single instruction, Multiple instructions
//! - Command Path: Simple, Multi-segment, Leading dot, No command path
//! - Arguments: Positional, Named, Mixed, None
//! - Argument Value: Unquoted, Quoted, Escaped, Invalid Escape
//! - Help Operator: Present, Absent
//! - Parser Options: `error_on_positional_after_named`, `error_on_duplicate_named_arguments`
//! - Error Conditions: Duplicate named args, Positional after named, Malformed named arg, Comments
//!
//! ---
//!
//! **Test Combinations: **
//!
//! | ID | Aspect Tested | Input String | Instruction Structure | Command Path | Arguments | Argument Value | Help Operator | Parser Options (`pos_after_named`, `dup_named`) | Error Condition | Expected Behavior |
//! |---|---|---|---|---|---|---|---|---|---|---|
//! | CT1.1 | Single instruction, unquoted positional arg | `cmd val` | Single | Simple (`cmd`) | Positional | Unquoted | Absent | `(false, false)` | None | Command `cmd`, Positional `val` |
//! | CT1.2 | Single instruction, multi-path, named arg | `path1 path2 name1 ::val1` | Single | Simple (`path1`) | Mixed | Unquoted | Absent | `(false, false)` | None | Command `path1`, Positional `path2`, Named `name1 ::val1` |
//! | CT1.3 | Single instruction, help operator | `cmd ?` | Single | Simple (`cmd`) | None | N/A | Present | `(false, false)` | None | Command `cmd`, Help requested |
//! | CT1.4 | Single instruction, quoted positional arg | `cmd "quoted val"` | Single | Simple (`cmd`) | Positional | Quoted | Absent | `(false, false)` | None | Command `cmd`, Positional `"quoted val"` |
//! | CT1.5 | Single instruction, named arg, escaped val | `cmd name1 :: "esc\nval"` | Single | Simple (`cmd`) | Named | Escaped | Absent | `(false, false)` | None | Command `cmd`, Named `name1 ::esc\nval` |
//! | CT1.6 | Single instruction, named arg, invalid escape | `cmd name1 :: "bad\xval"` | Single | Simple (`cmd`) | Named | Invalid Escape | Absent | `(false, false)` | None | Command `cmd`, Named `name1 ::bad\xval` (literal `\x`) |
//! | CT3.1 | Multi-instruction, basic separator | `cmd1 arg1 ;; cmd2 name ::val` | Multiple | Simple (`cmd1`), Simple (`cmd2`) | Positional, Named | Unquoted | Absent | `(false, false)` | None | Two instructions parsed correctly |
//! | CT4.1 | Duplicate named arg (error) | `cmd name ::val1 name ::val2` | Single | Simple (`cmd`) | Named | Unquoted | Absent | `(false, true)` | Duplicate named arg | Error: Duplicate named argument 'name' |
//! | CT4.2 | Duplicate named arg (last wins) | `cmd name ::val1 name ::val2` | Single | Simple (`cmd`) | Named | Unquoted | Absent | `(false, false)` | None | Last value wins: `val2` |
//! | CT5.1 | No path, named arg only (error) | `name ::val` | Single | No command path | Named | Unquoted | Absent | `(false, false)` | Malformed named arg | Error: Unexpected token ' :: ' in arguments |
//! | CT6.1 | Command path with dots and args | `cmd.sub.path arg1 name ::val` | Single | Multi-segment (`cmd.sub.path`) | Mixed | Unquoted | Absent | `(false, false)` | None | Command `cmd.sub.path`, Positional `arg1`, Named `name ::val` |
//! | SA1.1 | Root namespace list | `.` | Single | Leading dot | None | N/A | Absent | `(false, false)` | None | Empty command path, no args |
//! | SA1.2 | Root namespace help | `. ?` | Single | Leading dot | None | N/A | Present | `(false, false)` | None | Empty command path, help requested |
//! | SA2.1 | Whole line comment | `# this is a whole line comment` | Single | N/A | N/A | N/A | Absent | `(false, false)` | Comment | Error: Unexpected token '#' |
//! | SA2.2 | Comment only line | `#` | Single | N/A | N/A | N/A | Absent | `(false, false)` | Comment | Error: Unexpected token '#' |
//! | SA2.3 | Inline comment attempt | `cmd arg1 # inline comment` | Single | Simple (`cmd`) | Positional | N/A | Absent | `(false, false)` | Comment | Error: Unexpected token '#' |
use unilang_parser :: *;
use unilang_parser ::error :: { ErrorKind, SourceLocation };
// Removed: use unilang_parser ::error :: { ErrorKind, SourceLocation };
// Removed: use std ::collections ::HashMap;

fn options_error_on_duplicate_named() -> UnilangParserOptions
{
  UnilangParserOptions
  {
  error_on_duplicate_named_arguments: true,
  ..Default ::default()
 }
}

/// Tests a single instruction with a single command path and an unquoted positional argument.
/// Test Combination: CT1.1
#[ test ]
fn ct1_1_single_str_single_path_unquoted_pos_arg()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd val";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT1.1 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!( instruction.command_path_slices, vec![ "cmd".to_string() ], "CT1.1 Path" ); // Corrected expectation
  assert_eq!( instruction.positional_arguments.len(), 1, "CT1.1 Positional args count" );
  assert_eq!(
  instruction.positional_arguments[ 0 ].value,
  "val".to_string(),
  "CT1.1 Positional arg value"
 );
  assert!( instruction.named_arguments.is_empty(), "CT1.1 Named args" );
  // assert!(!instruction.help_requested, "CT1.1 Help requested");
}

/// Tests a single instruction with a multi-segment command path and an unquoted named argument.
/// Test Combination: CT1.2
#[ test ]
fn ct1_2_single_str_multi_path_unquoted_named_arg()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "path1 path2 name1 ::val1";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT1.2 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!( instruction.command_path_slices, vec![ "path1".to_string() ], "CT1.2 Path" ); // Corrected expectation
  assert_eq!( instruction.positional_arguments.len(), 1, "CT1.2 Positional args count" ); // Corrected expectation
  assert_eq!(
  instruction.positional_arguments[ 0 ].value,
  "path2".to_string(),
  "CT1.2 Positional arg value"
 ); // Corrected expectation
  assert_eq!( instruction.named_arguments.len(), 1, "CT1.2 Named args count" );
  let arg1 = instruction.named_arguments.get( "name1" ).expect( "CT1.2 Missing name1" );
  assert_eq!( arg1[0].value, "val1", "CT1.2 name1 value" ); // Changed to &str
      // assert!(!instruction.help_requested, "CT1.2 Help requested");
}

/// Tests a single instruction with a single command path and a help operator, no arguments.
/// Test Combination: CT1.3
#[ test ]
fn ct1_3_single_str_single_path_help_no_args()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd ?";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT1.3 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!( instruction.command_path_slices, vec![ "cmd".to_string() ], "CT1.3 Path" );
  assert!( instruction.positional_arguments.is_empty(), "CT1.3 Positional args" );
  assert!( instruction.named_arguments.is_empty(), "CT1.3 Named args" );
  assert!( instruction.help_requested, "CT1.3 Help requested should be true" ); // Re-enabled
}

/// Tests a single instruction with a single command path and a quoted positional argument.
/// Test Combination: CT1.4
#[ test ]
fn ct1_4_single_str_single_path_quoted_pos_arg()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd \"quoted val\"";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT1.4 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!( instruction.command_path_slices, vec![ "cmd".to_string() ], "CT1.4 Path" );
  assert_eq!( instruction.positional_arguments.len(), 1, "CT1.4 Positional args count" );
  assert_eq!(
  instruction.positional_arguments[ 0 ].value,
  "quoted val".to_string(),
  "CT1.4 Positional arg value"
 );
  assert!( instruction.named_arguments.is_empty(), "CT1.4 Named args" );
  // assert!(!instruction.help_requested, "CT1.4 Help requested");
}

/// Tests a single instruction with a single command path and a named argument with an escaped value.
/// Test Combination: CT1.5
#[ test ]
fn ct1_5_single_str_single_path_named_arg_escaped_val()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd name1 :: \"esc\\nval\"";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT1.5 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!( instruction.command_path_slices, vec![ "cmd".to_string() ], "CT1.5 Path" );
  assert!( instruction.positional_arguments.is_empty(), "CT1.5 Positional args" );
  assert_eq!( instruction.named_arguments.len(), 1, "CT1.5 Named args count" );
  let arg1 = instruction.named_arguments.get( "name1" ).expect( "CT1.5 Missing name1" );
  assert_eq!( arg1[0].value, "esc\nval", "CT1.5 name1 value with newline" ); // Changed to &str
               // assert!(!instruction.help_requested, "CT1.5 Help requested");
}

/// Tests a single instruction with a single command path and a named argument with an invalid escape sequence.
/// Test Combination: CT1.6
#[ test ]
fn ct1_6_single_str_single_path_named_arg_invalid_escape()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd name1 :: \"bad\\xval\"";
  let result = parser.parse_repl_input( input );
  assert!(
  result.is_ok(),
  "CT1.6 Expected Ok for invalid escape, got Err: {:?}",
  result.err()
 );
  let instruction = result.unwrap();
  assert_eq!(
  instruction.named_arguments.get( "name1" ).unwrap()[0].value,
  "bad\\xval".to_string(),
  "CT1.6 Invalid escape should be literal"
 );
}

/// Tests multiple instructions separated by `;;` with basic command and arguments.
/// Test Combination: CT3.1
#[ test ]
fn ct3_1_single_str_separator_basic()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd1 arg1 ;; cmd2 name ::val";
  let result = parser.parse_multiple_instructions( input ); // Changed to parse_multiple_instructions
  assert!( result.is_ok(), "CT3.1 Parse error: {:?}", result.err() );
  let instructions = result.unwrap();
  assert_eq!( instructions.len(), 2, "CT3.1 Instruction count" );

  // Instruction 1 : "cmd1 arg1" (Path: "cmd1", "arg1")
  let instr1 = &instructions[ 0 ];
  assert_eq!( instr1.command_path_slices, vec![ "cmd1".to_string() ], "CT3.1 Instr1 Path" ); // Corrected expectation
  assert_eq!( instr1.positional_arguments.len(), 1, "CT3.1 Instr1 Positional" ); // Corrected expectation
  assert_eq!(
  instr1.positional_arguments[ 0 ].value,
  "arg1".to_string(),
  "CT3.1 Positional arg value"
 ); // Corrected expectation
  assert!( instr1.named_arguments.is_empty(), "CT3.1 Instr1 Named" );
  // assert!(!instr1.help_requested);

  // Instruction 2 : "cmd2 name ::val"
  let instr2 = &instructions[ 1 ];
  assert_eq!( instr2.command_path_slices, vec![ "cmd2".to_string() ], "CT3.1 Instr2 Path" );
  assert!( instr2.positional_arguments.is_empty(), "CT3.1 Instr2 Positional" );
  assert_eq!( instr2.named_arguments.len(), 1, "CT3.1 Instr2 Named count" );
  assert_eq!(
  instr2.named_arguments.get( "name" ).unwrap()[0].value,
  "val",
  "CT3.1 Instr2 name value"
 ); // Changed to &str
              // assert!(!instr2.help_requested);
}

/// Tests that a duplicate named argument results in an error when the option is set.
/// Test Combination: CT4.1
#[ test ]
fn ct4_1_single_str_duplicate_named_error()
{
  let parser = Parser ::new( options_error_on_duplicate_named() );
  let input = "cmd name ::val1 name ::val2";
  let result = parser.parse_repl_input( input );
  assert!(
  result.is_err(),
  "CT4.1 Expected error for duplicate named, got Ok: {:?}",
  result.ok()
 );
  if let Err( e ) = result
  {
  assert!(
   matches!( e.kind, ErrorKind ::Syntax( _ ) ),
   "CT4.1 ErrorKind mismatch: {:?}",
   e.kind
 );
  assert!(
   e.to_string().contains( "Duplicate named argument 'name'" ),
   "CT4.1 Error message mismatch: {e}"
 );
 }
}

/// Tests that the last value wins for duplicate named arguments when the option is not set.
/// Test Combination: CT4.2
#[ test ]
fn ct4_2_single_str_duplicate_named_last_wins()
{
  let parser = Parser ::new( UnilangParserOptions
  {
  error_on_duplicate_named_arguments: false,
  ..Default ::default()
 }); // Explicitly set to false
  let input = "cmd name ::val1 name ::val2";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT4.2 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!( instruction.command_path_slices, vec![ "cmd".to_string() ] );
  assert_eq!( instruction.named_arguments.len(), 1, "CT4.2 Named args count" );
  assert_eq!(
  instruction.named_arguments.get( "name" ).unwrap().last().unwrap().value,
  "val2",
  "CT4.2 Last value should win"
 ); // Changed to &str
}

/// Tests that an instruction with no command path but only a named argument parses successfully.
/// Test Combination: CT5.1
/// Fixed(issue-cmd-path): This test was incorrectly expecting an error.
/// Per spec.md:173, command path is optional, so named-only args are valid.
#[ test ]
fn ct5_1_single_str_no_path_named_arg_only()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "name ::val";
  let result = parser.parse_repl_input( input );
  assert!(
  result.is_ok(),
  "CT5.1 Named-only args should parse successfully (spec.md:173), got error: {:?}",
  result.err()
 );
  let inst = result.unwrap();
  assert!( inst.command_path_slices.is_empty(), "CT5.1 Command path should be empty" );
  assert!( inst.named_arguments.contains_key( "name" ), "CT5.1 Should have 'name' named arg" );
  assert_eq!( inst.named_arguments.get( "name" ).unwrap()[0].value, "val", "CT5.1 Value should be 'val'" );
}

/// Tests a command path with dots and arguments.
/// Test Combination: CT6.1
#[ test ]
fn ct6_1_command_path_with_dots_and_slashes()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd.sub.path arg1 name ::val"; // Changed input to use only dots for path
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "CT6.1 Parse error: {:?}", result.err() );
  let instruction = result.unwrap();
  assert_eq!(
  instruction.command_path_slices,
  vec![ "cmd".to_string(), "sub".to_string(), "path".to_string() ],
  "CT6.1 Path"
 ); // Corrected expectation
  assert_eq!( instruction.positional_arguments.len(), 1, "CT6.1 Positional args count" ); // Corrected expectation
  assert_eq!(
  instruction.positional_arguments[ 0 ].value,
  "arg1".to_string(),
  "CT6.1 Positional arg value"
 ); // Corrected expectation
  assert_eq!( instruction.named_arguments.len(), 1, "CT6.1 Named args count" );
  assert_eq!(
  instruction.named_arguments.get( "name" ).unwrap()[0].value,
  "val",
  "CT6.1 name value"
 ); // Changed to &str
              // assert!(!instruction.help_requested, "CT6.1 Help requested");
}

/// Tests parsing of a root namespace list instruction (input '.').
/// Test Combination: SA1.1
#[ test ]
fn sa1_1_root_namespace_list()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = ".";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "SA1.1 Parse error for '.' : {:?}", result.err() );
  let instruction = result.unwrap();
  assert!(
  instruction.command_path_slices.is_empty(),
  "SA1.1 Path for '.' should be empty"
 );
  assert!(
  instruction.positional_arguments.is_empty(),
  "SA1.1 Positional args for '.' should be empty"
 );
  assert!(
  instruction.named_arguments.is_empty(),
  "SA1.1 Named args for '.' should be empty"
 );
  assert_eq!( instruction.overall_location, SourceLocation ::StrSpan { start: 0, end: 1 } );
}

/// Tests parsing of a root namespace help instruction (input '. ?').
/// Test Combination: SA1.2
#[ test ]
fn sa1_2_root_namespace_help()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = ". ?";
  let result = parser.parse_repl_input( input );
  assert!( result.is_ok(), "SA1.2 Parse error for '. ?' : {:?}", result.err() );
  let instruction = result.unwrap();
  // Expecting path to be empty, no positional args, and help requested.
  assert!(
  instruction.command_path_slices.is_empty(),
  "SA1.2 Path for '. ?' should be empty"
 );
  assert!(
  instruction.positional_arguments.is_empty(),
  "SA1.2 Positional args for '. ?' should be empty"
 );
  assert!( instruction.help_requested, "SA1.2 Help requested for '. ?' should be true" );
  // Re-enabled
}

/// Tests that a whole line comment results in an error.
/// Test Combination: SA2.1
#[ test ]
fn sa2_1_whole_line_comment()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "# this is a whole line comment";
  let result = parser.parse_repl_input( input );
  assert!(
  result.is_err(),
  "SA2.1 Expected error for whole line comment, got Ok: {:?}",
  result.ok()
 );
  if let Err( e ) = result
  {
  assert!(
   matches!( e.kind, ErrorKind ::Syntax( _ ) ),
   "SA2.1 ErrorKind mismatch: {:?}",
   e.kind
 );
  assert!(
   e.to_string().contains( "Unexpected token '#' in arguments" ),
   "SA2.1 Error message mismatch: {e}"
 );
 }
}

/// Tests that a line with only a comment character results in an error.
/// Test Combination: SA2.2
#[ test ]
fn sa2_2_comment_only_line()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "#";
  let result = parser.parse_repl_input( input );
  assert!(
  result.is_err(),
  "SA2.2 Expected error for '#' only line, got Ok: {:?}",
  result.ok()
 );
  if let Err( e ) = result
  {
  assert!(
   matches!( e.kind, ErrorKind ::Syntax( _ ) ),
   "SA2.2 ErrorKind mismatch: {:?}",
   e.kind
 );
  assert!(
   e.to_string().contains( "Unexpected token '#' in arguments" ),
   "SA2.2 Error message mismatch: {e}"
 );
 }
}

/// Tests that an inline comment attempt results in an error.
/// Test Combination: SA2.3
#[ test ]
fn sa2_3_inline_comment_attempt()
{
  let parser = Parser ::new( UnilangParserOptions ::default() );
  let input = "cmd arg1 # inline comment";
  let result = parser.parse_repl_input( input );
  assert!(
  result.is_err(),
  "SA2.3 Expected error for inline '#', got Ok: {:?}",
  result.ok()
 );
  if let Err( e ) = result
  {
  assert!(
   matches!( e.kind, ErrorKind ::Syntax( _ ) ),
   "SA2.3 ErrorKind mismatch: {:?}",
   e.kind
 );
  assert!(
   e.to_string().contains( "Unexpected token '#' in arguments" ),
   "SA2.3 Error message mismatch: {e}"
 ); // Changed message
 }
}

// TDD Phase 1 — written BEFORE parse_repl_input is implemented (RED)
#[ test ]
fn parse_repl_input_exists_and_delegates_to_string_parser()
{
  let parser = Parser::new( UnilangParserOptions::default() );
  let result_old = parser.parse_repl_input( ".cmd key::val" );
  let result_new = parser.parse_repl_input( ".cmd key::val" );
  assert_eq!(
    result_old.is_ok(),
    result_new.is_ok(),
    "parse_repl_input must delegate identically to parse_single_instruction"
  );
}