kaish-kernel 0.14.1

Core kernel for kaish: lexer, parser, interpreter, and runtime
Documentation
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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
//! Kernel-routed tests for `printf` format flags, precision, and conversions.
//!
//! These test the P1 printf bugs:
//! - `+`/space/`#` flags ignored
//! - `.precision` ignored for `%s` and `%d`/`%x`/`%o`
//! - precision doesn't override `0` flag
//! - `%E`/`%G`/`%b`/`%u` emit literal `%X`
//! - `\0NNN` octal escape broken
//!
//! Each test routes through `kernel.execute()` so the full pipeline runs.
//! Expected values verified against GNU `printf(1)`.

#![allow(clippy::unwrap_used, clippy::expect_used)]

use kaish_kernel::{Kernel, KernelConfig};

fn kernel() -> Kernel {
    Kernel::new(KernelConfig::transient().with_skip_validation(true)).expect("kernel")
}

// ---------------------------------------------------------------------------
// Sign flags: + and space
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_plus_flag_positive() {
    // printf '%+d' 5 → +5
    let k = kernel();
    let result = k.execute("printf '%+d' 5").await.expect("execute");
    assert_eq!(result.text_out(), "+5", "printf '%+d' 5 should produce +5");
}

#[tokio::test]
async fn printf_plus_flag_negative() {
    // printf '%+d' -5 → -5 (minus wins over +)
    let k = kernel();
    let result = k.execute("printf '%+d' -- -5").await.expect("execute");
    assert_eq!(result.text_out(), "-5", "printf '%+d' -5 should produce -5");
}

#[tokio::test]
async fn printf_space_flag_positive() {
    // printf '% d' 5 → " 5"
    let k = kernel();
    let result = k.execute("printf '% d' 5").await.expect("execute");
    assert_eq!(result.text_out(), " 5", "printf '% d' 5 should produce ' 5'");
}

#[tokio::test]
async fn printf_space_flag_negative() {
    // printf '% d' -5 → "-5" (minus wins over space)
    let k = kernel();
    let result = k.execute("printf '% d' -- -5").await.expect("execute");
    assert_eq!(result.text_out(), "-5", "printf '% d' -5 should produce -5");
}

#[tokio::test]
async fn printf_plus_beats_space() {
    // when both + and space are specified, + wins
    let k = kernel();
    let result = k.execute("printf '% +d' 5").await.expect("execute");
    assert_eq!(result.text_out(), "+5", "printf '% +d' 5: + flag should win over space");
}

// ---------------------------------------------------------------------------
// Alternate form (#) flag
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_hash_hex_lower() {
    // printf '%#x' 255 → 0xff
    let k = kernel();
    let result = k.execute("printf '%#x' 255").await.expect("execute");
    assert_eq!(result.text_out(), "0xff", "printf '%#x' 255 should produce 0xff");
}

#[tokio::test]
async fn printf_hash_hex_upper() {
    // printf '%#X' 255 → 0XFF
    let k = kernel();
    let result = k.execute("printf '%#X' 255").await.expect("execute");
    assert_eq!(result.text_out(), "0XFF", "printf '%#X' 255 should produce 0XFF");
}

#[tokio::test]
async fn printf_hash_octal() {
    // printf '%#o' 8 → 010
    let k = kernel();
    let result = k.execute("printf '%#o' 8").await.expect("execute");
    assert_eq!(result.text_out(), "010", "printf '%#o' 8 should produce 010");
}

#[tokio::test]
async fn printf_hash_zero_is_zero() {
    // printf '%#x' 0 → 0  (special case: 0 with # stays 0, not 0x0)
    let k = kernel();
    let result = k.execute("printf '%#x' 0").await.expect("execute");
    assert_eq!(result.text_out(), "0", "printf '%#x' 0 should produce 0 not 0x0");
}

// ---------------------------------------------------------------------------
// Precision for %d/%x/%o (zero-pads numeric digits)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_precision_decimal() {
    // printf '%.3d' 5 → 005
    let k = kernel();
    let result = k.execute("printf '%.3d' 5").await.expect("execute");
    assert_eq!(result.text_out(), "005", "printf '%.3d' 5 should produce 005");
}

#[tokio::test]
async fn printf_precision_decimal_zero() {
    // printf '%.5d' 0 → 00000
    let k = kernel();
    let result = k.execute("printf '%.5d' 0").await.expect("execute");
    assert_eq!(result.text_out(), "00000", "printf '%.5d' 0 should produce 00000");
}

#[tokio::test]
async fn printf_precision_hex() {
    // printf '%.4x' 255 → 00ff
    let k = kernel();
    let result = k.execute("printf '%.4x' 255").await.expect("execute");
    assert_eq!(result.text_out(), "00ff", "printf '%.4x' 255 should produce 00ff");
}

#[tokio::test]
async fn printf_precision_overrides_zero_flag() {
    // When precision is specified, 0 flag is ignored for integers
    // printf '%05.3d' 5 → "  005" (width 5, precision 3, no zero-pad because precision given)
    let k = kernel();
    let result = k.execute("printf '%05.3d' 5").await.expect("execute");
    assert_eq!(result.text_out(), "  005", "printf '%05.3d' 5: precision overrides zero flag");
}

// ---------------------------------------------------------------------------
// Precision for %s (truncates string)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_precision_string() {
    // printf '%.3s' abcdef → abc
    let k = kernel();
    let result = k.execute("printf '%.3s' abcdef").await.expect("execute");
    assert_eq!(result.text_out(), "abc", "printf '%.3s' abcdef should produce abc");
}

#[tokio::test]
async fn printf_precision_string_shorter_than_precision() {
    // printf '%.10s' hello → hello (no truncation when string is shorter)
    let k = kernel();
    let result = k.execute("printf '%.10s' hello").await.expect("execute");
    assert_eq!(result.text_out(), "hello", "printf '%.10s' hello should produce hello");
}

// ---------------------------------------------------------------------------
// Precision for %f (already worked but covering for regression)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_precision_float() {
    // printf '%.2f' 3.14159 → 3.14
    let k = kernel();
    let result = k.execute("printf '%.2f' 3.14159").await.expect("execute");
    assert_eq!(result.text_out(), "3.14", "printf '%.2f' 3.14159 should produce 3.14");
}

// ---------------------------------------------------------------------------
// %u unsigned decimal
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_u_conversion_positive() {
    // printf '%u' 5 → 5
    let k = kernel();
    let result = k.execute("printf '%u' 5").await.expect("execute");
    assert_eq!(result.text_out(), "5", "printf '%u' 5 should produce 5");
}

// ---------------------------------------------------------------------------
// %E uppercase scientific notation
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_big_e_conversion() {
    // printf '%E' 1000 → 1.000000E+03
    let k = kernel();
    let result = k.execute("printf '%E' 1000").await.expect("execute");
    assert_eq!(result.text_out(), "1.000000E+03", "printf '%E' 1000 should produce 1.000000E+03");
}

#[tokio::test]
async fn printf_big_e_negative() {
    // printf '%E' -1000 → -1.000000E+03
    let k = kernel();
    let result = k.execute("printf '%E' -- -1000").await.expect("execute");
    assert_eq!(result.text_out(), "-1.000000E+03", "printf '%E' -1000 should produce -1.000000E+03");
}

#[tokio::test]
async fn printf_big_e_with_precision() {
    // printf '%.2E' 1000 → 1.00E+03
    let k = kernel();
    let result = k.execute("printf '%.2E' 1000").await.expect("execute");
    assert_eq!(result.text_out(), "1.00E+03", "printf '%.2E' 1000 should produce 1.00E+03");
}

// ---------------------------------------------------------------------------
// %G uppercase %g (shorter of %E or %f, remove trailing zeros)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_big_g_large_number() {
    // printf '%G' 1234567 → 1.23457E+06
    let k = kernel();
    let result = k.execute("printf '%G' 1234567").await.expect("execute");
    assert_eq!(result.text_out(), "1.23457E+06", "printf '%G' 1234567 should produce 1.23457E+06");
}

#[tokio::test]
async fn printf_big_g_small_number() {
    // printf '%G' 0.000123 → 0.000123
    let k = kernel();
    let result = k.execute("printf '%G' 0.000123").await.expect("execute");
    assert_eq!(result.text_out(), "0.000123", "printf '%G' 0.000123 should produce 0.000123");
}

#[tokio::test]
async fn printf_big_g_one() {
    // printf '%G' 1.0 → 1
    let k = kernel();
    let result = k.execute("printf '%G' 1.0").await.expect("execute");
    assert_eq!(result.text_out(), "1", "printf '%G' 1.0 should produce 1");
}

// ---------------------------------------------------------------------------
// %b — interpret backslash escapes in the argument
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_b_tab_escape() {
    // printf '%b' '\t' → TAB character
    let k = kernel();
    let result = k.execute(r#"printf '%b' '\t'"#).await.expect("execute");
    assert_eq!(result.text_out(), "\t", "printf '%b' '\\t' should produce a tab");
}

#[tokio::test]
async fn printf_b_newline_escape() {
    // printf '%b' 'hello\nworld' → "hello\nworld" with actual newline
    let k = kernel();
    let result = k.execute(r#"printf '%b' 'hello\nworld'"#).await.expect("execute");
    assert_eq!(result.text_out(), "hello\nworld", "printf '%b' 'hello\\nworld' should have a real newline");
}

#[tokio::test]
async fn printf_b_octal_escape() {
    // printf '%b' '\101' → A (octal 101 = 65 = 'A')
    let k = kernel();
    let result = k.execute(r#"printf '%b' '\101'"#).await.expect("execute");
    assert_eq!(result.text_out(), "A", "printf '%b' '\\101' should produce A");
}

#[tokio::test]
async fn printf_b_plain_string_passthrough() {
    // printf '%b' 'hello' → hello (no escapes to interpret)
    let k = kernel();
    let result = k.execute(r#"printf '%b' 'hello'"#).await.expect("execute");
    assert_eq!(result.text_out(), "hello", "printf '%b' 'hello' should produce hello");
}

// ---------------------------------------------------------------------------
// \0NNN octal escape in format string
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_format_octal_escape_zero_prefix() {
    // `\0NNN`: the leading `0` is the FIRST of up to 3 octal digits, so at most
    // 2 more follow — matching GNU coreutils, bash, and dash (all verified).
    // `\0101` → octal `010` (BS) then a literal `1`; NOT octal 101 ('A').
    let k = kernel();
    let result = k.execute(r#"printf '\0101'"#).await.expect("execute");
    assert_eq!(
        result.text_out(),
        "\u{8}1",
        "printf '\\0101' → BS + '1' (octal 010, then literal 1), like GNU printf"
    );
    // \012 = octal 12 = newline (2 digits after the 0).
    assert_eq!(
        kernel().execute(r#"printf '\012'"#).await.expect("execute").text_out(),
        "\n",
    );
    // \0377 → octal 037 (0x1f) then a literal '7'.
    assert_eq!(
        kernel().execute(r#"printf '\0377'"#).await.expect("execute").text_out(),
        "\u{1f}7",
    );
    // Bare \NNN (no leading 0) still takes the full 3 digits: \101 = 'A'.
    assert_eq!(
        kernel().execute(r#"printf '\101'"#).await.expect("execute").text_out(),
        "A",
    );
}

#[tokio::test]
async fn printf_format_octal_escape_no_zero_prefix() {
    // printf '\101' → A (same as above but without leading 0)
    let k = kernel();
    let result = k.execute(r#"printf '\101'"#).await.expect("execute");
    assert_eq!(result.text_out(), "A", "printf '\\101' should produce A");
}

// ---------------------------------------------------------------------------
// Width + sign flag combinations
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_width_plus_sign() {
    // printf '%+6d' 5 → "    +5"
    let k = kernel();
    let result = k.execute("printf '%+6d' 5").await.expect("execute");
    assert_eq!(result.text_out(), "    +5", "printf '%+6d' 5 should produce '    +5'");
}

#[tokio::test]
async fn printf_zero_pad_plus_sign() {
    // printf '%+06d' 5 → "+00005"
    let k = kernel();
    let result = k.execute("printf '%+06d' 5").await.expect("execute");
    assert_eq!(result.text_out(), "+00005", "printf '%+06d' 5 should produce +00005");
}

#[tokio::test]
async fn printf_left_align_beats_zero_pad() {
    // printf '%0-5d' 42 → "42   " (left align wins over zero pad)
    let k = kernel();
    let result = k.execute("printf '%-05d' 42").await.expect("execute");
    assert_eq!(result.text_out(), "42   ", "printf '%-05d' 42: left-align should win over zero-pad");
}

// ---------------------------------------------------------------------------
// %e lowercase scientific (regression guard — was probably already wrong)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_little_e_conversion() {
    // printf '%e' 1000 → 1.000000e+03
    let k = kernel();
    let result = k.execute("printf '%e' 1000").await.expect("execute");
    assert_eq!(result.text_out(), "1.000000e+03", "printf '%e' 1000 should produce 1.000000e+03");
}

// ---------------------------------------------------------------------------
// %g lowercase (regression guard)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_little_g_large_number() {
    // printf '%g' 1234567 → 1.23457e+06
    let k = kernel();
    let result = k.execute("printf '%g' 1234567").await.expect("execute");
    assert_eq!(result.text_out(), "1.23457e+06", "printf '%g' 1234567 should produce 1.23457e+06");
}

// ---------------------------------------------------------------------------
// Dash-only format operand (GH #137 sibling — same lexer bug as `echo ---`)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_dash_only_literal() {
    // printf --- (unquoted) → literal "---" as the format string, no
    // conversions. Used to print "-" (the lexer's plain `--` literal
    // swallowed the leading two dashes as a spurious end-of-flags marker).
    let k = kernel();
    let result = k.execute("printf ---").await.expect("execute");
    assert_eq!(result.text_out(), "---", "printf --- should produce ---");
}

#[tokio::test]
async fn printf_dash_only_after_double_dash() {
    // printf -- --- : a real `--` end-of-flags marker followed by the
    // dash-only operand. Used to parse-error (a spurious second `--` token
    // fell out of mis-lexing `---`, with no grammar production for it).
    let k = kernel();
    let result = k.execute("printf -- ---").await.expect("execute");
    assert_eq!(result.text_out(), "---", "printf -- --- should produce ---");
}

// ---------------------------------------------------------------------------
// GH #154: %Ns width padding must use display width, not UTF-8 byte length.
// `apply_string_padding` compared `width` against `val.len()` (bytes), so a
// CJK/emoji argument — whose byte length exceeds its display width — got
// under-padded (or not padded at all), misaligning output the same way the
// REPL's table renderer did pre-#130.
// ---------------------------------------------------------------------------

#[tokio::test]
async fn printf_width_ascii_regression() {
    // Plain ASCII: byte length == display width == char count, so this must
    // keep working exactly as before.
    let k = kernel();
    let result = k.execute("printf '%10s' hi").await.expect("execute");
    assert_eq!(result.text_out(), "        hi", "printf '%10s' hi should right-pad to width 10");
}

#[tokio::test]
async fn printf_width_cjk_right_align() {
    // 日本語 is 3 chars / 9 UTF-8 bytes / 6 display columns (2 cols each).
    // Width 10 should pad with 10 - 6 = 4 spaces, not 10 - 9 = 1.
    // (The lexer's bareword regex is ASCII-only, so the CJK operand is quoted
    // — same as any other kaish argument containing non-ASCII text.)
    let k = kernel();
    let result = k.execute("printf '%10s' '日本語'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "    日本語",
        "printf '%10s' 日本語 should pad to display width 10 (4 spaces + 6-wide string)"
    );
}

#[tokio::test]
async fn printf_width_cjk_left_align() {
    // Left-aligned: 日本語 (6 display cols) + 4 trailing spaces to reach width 10.
    let k = kernel();
    let result = k.execute("printf '%-10s' '日本語'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "日本語    ",
        "printf '%-10s' 日本語 should left-align and pad to display width 10"
    );
}

#[tokio::test]
async fn printf_width_emoji() {
    // A single emoji (🎉, U+1F389) is 1 char / 4 UTF-8 bytes / 2 display
    // columns. Width 5 should pad with 5 - 2 = 3 spaces, not 5 - 4 = 1.
    let k = kernel();
    let result = k.execute("printf '%5s' '🎉'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "   🎉",
        "printf '%5s' 🎉 should pad to display width 5 (3 spaces + 2-wide emoji)"
    );
}

#[tokio::test]
async fn printf_width_c_conversion_cjk() {
    // %c shares apply_string_padding with %s — a wide single character must
    // pad the same way.
    let k = kernel();
    let result = k.execute("printf '%5c' '日'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "",
        "printf '%5c' 日 should pad to display width 5 (3 spaces + 2-wide char)"
    );
}

#[tokio::test]
async fn printf_precision_multibyte_does_not_panic_or_split_char() {
    // %.Ns precision truncates by character count (`chars().take(prec)`), not
    // byte index, so it can never land mid-codepoint. Guard this explicitly:
    // 日本語 truncated to precision 2 must yield exactly the first two
    // characters ("日本"), not a byte-sliced partial UTF-8 sequence (which
    // would either panic on a non-char-boundary slice or emit invalid bytes).
    let k = kernel();
    let result = k.execute("printf '%.2s' '日本語'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "日本",
        "printf '%.2s' 日本語 should truncate to the first 2 characters without splitting a char"
    );
}

#[tokio::test]
async fn printf_zero_pad_width_cjk_counts_display_width() {
    // apply_string_padding's zero-pad branch (`spec.zero_pad`, no left-align)
    // prepends `pad_count` '0' chars before the value; `pad_count` must come
    // from display width like the space-padding branches, not byte length.
    // 日本語 is 6 display columns, so width 10 zero-pads with 10 - 6 = 4
    // zeros, not 10 - 9 (byte length) = 1.
    let k = kernel();
    let result = k.execute("printf '%010s' '日本語'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "0000日本語",
        "printf '%010s' 日本語 should zero-pad to display width 10 (4 zeros + 6-wide string)"
    );
}

#[tokio::test]
async fn printf_width_and_precision_multibyte_pads_by_truncated_width() {
    // Combined width + precision: truncation happens first (in apply_specifier's
    // 's' arm), then apply_string_padding measures the ALREADY-TRUNCATED value's
    // display width — not the original argument's width. 日本語 truncated to
    // precision 2 is "日本" (4 display columns, 2 chars), so width 10 pads with
    // 10 - 4 = 6 spaces, not 10 - 6 (untruncated width) = 4.
    let k = kernel();
    let result = k.execute("printf '%10.2s' '日本語'").await.expect("execute");
    assert_eq!(
        result.text_out(),
        "      日本",
        "printf '%10.2s' 日本語 should pad based on the truncated value's display width (6 spaces + 4-wide '日本')"
    );
}

#[tokio::test]
async fn printf_width_zero_width_codepoint_no_underflow() {
    // A combining mark (U+0301 COMBINING ACUTE ACCENT) has display width 0
    // per unicode-width. `width - display_width` must not underflow/panic
    // when display_width is 0 — it should just pad with the full width's
    // worth of spaces, same as an empty string would.
    let k = kernel();
    let arg = "\u{0301}"; // lone combining acute accent: 2 UTF-8 bytes, 1 char, 0 display columns
    let cmd = format!("printf '%5s' '{arg}'");
    let result = k.execute(&cmd).await.expect("execute");
    assert_eq!(
        result.text_out(),
        format!("     {arg}"),
        "printf '%5s' with a zero-width combining mark should pad with all 5 spaces, no underflow/panic"
    );
}