-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfish-shell-why-not-rust.html
More file actions
919 lines (887 loc) · 115 KB
/
Copy pathfish-shell-why-not-rust.html
File metadata and controls
919 lines (887 loc) · 115 KB
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
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
<!doctype html>
<html lang="en" data-theme="dark" data-lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>why-not-rust · fish-shell/fish-shell</title>
<style>
:root[data-theme="dark"] {
color-scheme: dark;
--page:#0a0a0b; --raised:#111113; --line:#232327; --line-soft:#1a1a1d;
--ink:#f4f4f5; --ink-2:#a8a8b0; --ink-3:#6e6e78;
--blue:#5b9cf0; --teal:#3fbfa0; --rust:#f0803c; --amber:#e0a93c;
--wash:rgba(255,255,255,.028);
}
:root[data-theme="light"] {
color-scheme: light;
--page:#fcfcfb; --raised:#ffffff; --line:#e4e2dd; --line-soft:#efedE8;
--ink:#16161a; --ink-2:#55555f; --ink-3:#8b8b95;
--blue:#1f6fd0; --teal:#0f8f74; --rust:#c85a15; --amber:#9a6b00;
--wash:rgba(0,0,0,.022);
}
:root {
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif;
--accent:var(--blue);
--measure:74ch;
}
body.v-stay { --accent:var(--blue); }
body.v-extract { --accent:var(--teal); }
body.v-partial { --accent:var(--amber); }
body.v-migrate { --accent:var(--rust); }
* { box-sizing:border-box; margin:0; padding:0; }
html { background:var(--page); }
body {
background:var(--page); color:var(--ink); font-family:var(--sans);
font-size:16px; line-height:1.62; -webkit-font-smoothing:antialiased;
padding:0 32px 96px;
/* Evidence is full of long file paths, commit SHAs and URLs. Without this
a single unbreakable token pushes the page wider than a phone screen. */
overflow-wrap:break-word;
}
.wrap { max-width:1080px; margin:0 auto; }
.mono { font-family:var(--mono); font-variant-ligatures:none; }
a { color:inherit; }
/* ---------- masthead ---------- */
.mast { display:flex; align-items:center; gap:14px; padding:26px 0 22px; }
.logo { font-size:.9rem; font-weight:600; letter-spacing:-.02em; }
.logo .q { color:var(--accent); }
.mast .meta { margin-left:auto; display:flex; gap:14px; align-items:center; color:var(--ink-3); font-size:.78rem; }
.btn {
font:inherit; font-size:.72rem; color:var(--ink-2); background:none; cursor:pointer;
border:1px solid var(--line); border-radius:6px; padding:3px 9px; line-height:1.5;
}
.btn:hover { color:var(--ink); border-color:var(--ink-3); }
/* ---------- hero ---------- */
.hero { padding:22px 0 40px; border-top:1px solid var(--line); }
.kicker {
font-size:.7rem; letter-spacing:.13em; text-transform:uppercase;
color:var(--ink-3); font-weight:600;
}
.kicker .project { color:var(--ink-2); text-transform:none; letter-spacing:.02em; }
.hero-grid { display:grid; grid-template-columns:minmax(0,1fr) 330px; gap:56px; margin-top:26px; align-items:start; }
.scope-word {
font-size:clamp(3.2rem,8vw,4.6rem); line-height:.94; font-weight:760;
letter-spacing:-.045em; color:var(--accent);
}
.scope-sub { margin-top:12px; font-size:1.02rem; color:var(--ink); font-weight:500; }
.auth {
display:inline-flex; align-items:center; gap:8px; margin-top:20px;
font-size:.74rem; font-weight:650; letter-spacing:.05em; color:var(--ink-2);
}
.auth i { width:7px; height:7px; border-radius:50%; background:var(--ink-3); flex:none; }
body.a-approve .auth i { background:var(--teal); }
body.a-reject .auth i { background:var(--blue); }
body.a-defer .auth i { background:var(--amber); }
.why { margin-top:18px; font-size:1.02rem; line-height:1.66; color:var(--ink); max-width:var(--measure); }
.trigger {
margin-top:16px; padding-left:15px; border-left:2px solid var(--line);
font-size:.87rem; line-height:1.6; color:var(--ink-2); max-width:var(--measure);
}
.chips { display:flex; flex-wrap:wrap; gap:9px 20px; margin-top:26px; font-size:.75rem; color:var(--ink-3); }
.chips b { color:var(--ink-2); font-weight:600; }
.chips .scope b { color:var(--accent); }
/* ---------- gates ---------- */
.gatebox h2 {
font-size:.68rem; letter-spacing:.13em; text-transform:uppercase;
color:var(--ink-3); font-weight:650; padding-bottom:11px; border-bottom:1px solid var(--line);
}
.gate { display:grid; grid-template-columns:26px 1fr; gap:11px; padding:13px 0; border-bottom:1px solid var(--line-soft); align-items:start; }
.gate:last-child { border-bottom:0; }
.gate .id { color:var(--ink-3); font-size:.7rem; padding-top:3px; }
.gate .head { display:flex; align-items:center; gap:8px; }
.gate .name { font-size:.88rem; font-weight:620; }
.gate .ev { color:var(--ink-3); font-size:.78rem; line-height:1.5; margin-top:3px; }
/* state words: a dot plus a word. No filled pills anywhere. */
.state { display:inline-flex; align-items:center; gap:5px; font-size:.66rem; font-weight:700; letter-spacing:.07em; white-space:nowrap; }
.state::before { content:""; width:6px; height:6px; border-radius:50%; background:currentColor; flex:none; }
.state.pass { color:var(--teal); }
.state.fail { color:var(--blue); }
.state.unknown { color:var(--amber); }
/* ---------- magnitude figures ---------- */
.tiles {
/* Six magnitude tiles is the house style, and 150px + a 24px gutter is what
lets all six sit on one row inside the 1080px measure. At 168/34 the sixth
tile orphaned onto a row of its own on every report. */
display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
gap:30px 24px; padding:32px 0; border-top:1px solid var(--line); border-bottom:1px solid var(--line);
}
.tile .label { font-size:.72rem; color:var(--ink-3); line-height:1.4; }
/* Sized so a nine-character value — which is 117 of the 120 tiles in the
gallery — stays on one line in a six-across row. Longer composite values
("16,350–808,625") wrap at a space, which no usable size prevents. */
.tile .value { margin-top:7px; font-size:1.55rem; font-weight:640; letter-spacing:-.035em; line-height:1.12; }
/* The unit is a single token ("lines", "2016–2021"); never break inside it. */
.tile .unit { font-size:.8rem; font-weight:500; color:var(--ink-3); margin-left:5px; letter-spacing:0; white-space:nowrap; }
.tile .note { margin-top:6px; font-size:.71rem; color:var(--ink-3); line-height:1.45; }
/* ---------- sections ---------- */
section { padding-top:58px; }
.section-title { display:flex; align-items:baseline; gap:12px; font-size:1.18rem; font-weight:640; letter-spacing:-.02em; }
.section-title .no { font-family:var(--mono); font-size:.72rem; color:var(--ink-3); font-weight:500; }
.sub { margin-top:9px; font-size:.85rem; color:var(--ink-3); max-width:var(--measure); line-height:1.6; }
/* ---------- options table ---------- */
.table-wrap { overflow-x:auto; margin-top:22px; }
table { width:100%; border-collapse:collapse; font-size:.79rem; min-width:820px; }
th {
text-align:left; font-size:.66rem; letter-spacing:.1em; text-transform:uppercase;
color:var(--ink-3); font-weight:650; padding:0 14px 10px 0; border-bottom:1px solid var(--line);
}
td { padding:15px 14px 15px 0; border-bottom:1px solid var(--line-soft); vertical-align:top; color:var(--ink-2); line-height:1.5; }
tr:last-child td { border-bottom:0; }
tr.selected td { background:var(--wash); }
tr.selected td:first-child { box-shadow:inset 2px 0 0 var(--accent); padding-left:13px; }
tr.excluded td { opacity:.6; }
.option-name { color:var(--ink); font-weight:620; display:block; }
td .source { color:var(--ink-3); font-size:.72rem; margin-top:3px; display:block; line-height:1.45; }
.scope-tag { font-family:var(--mono); font-size:.66rem; letter-spacing:.05em; color:var(--ink-3); }
.ev-tag { font-size:.66rem; font-weight:700; letter-spacing:.06em; }
.ev-tag.strong { color:var(--teal); } .ev-tag.moderate { color:var(--ink-2); }
.ev-tag.weak { color:var(--amber); } .ev-tag.unknown { color:var(--amber); }
/* ---------- twelve-lens ledger ---------- */
.ledger { margin-top:22px; border-top:1px solid var(--line); }
.lens { display:grid; grid-template-columns:186px minmax(0,1fr); gap:26px; padding:19px 0; border-bottom:1px solid var(--line-soft); align-items:start; }
.lens .name { font-size:.82rem; font-weight:620; color:var(--ink); }
.lens .tags { display:flex; flex-direction:column; gap:5px; margin-top:7px; }
.dir-tag { font-size:.66rem; font-weight:650; letter-spacing:.04em; line-height:1.4; }
.dir-tag.rust { color:var(--rust); } .dir-tag.current { color:var(--blue); }
.dir-tag.neutral { color:var(--ink-3); } .dir-tag.unknown { color:var(--amber); }
.lens .claim { font-size:.87rem; color:var(--ink-2); line-height:1.6; }
.lens .source { margin-top:6px; font-size:.72rem; color:var(--ink-3); word-break:break-word; }
/* ---------- evidence cards ---------- */
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(290px,1fr)); gap:26px 34px; margin-top:24px; }
.card { padding-left:15px; border-left:2px solid var(--line); }
.card.rust { border-left-color:var(--rust); }
.card.current { border-left-color:var(--blue); }
.card.unknown { border-left-color:var(--amber); }
.card h3 { font-size:.9rem; font-weight:620; letter-spacing:-.01em; }
.card p { margin-top:7px; font-size:.85rem; color:var(--ink-2); line-height:1.6; }
.card .ref { display:block; margin-top:9px; font-size:.7rem; color:var(--ink-3); word-break:break-word; }
/* ---------- buys / doesn't buy ---------- */
.duo { display:grid; grid-template-columns:1fr 1fr; gap:44px; margin-top:24px; }
.col h3 { display:flex; align-items:center; gap:8px; font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-3); font-weight:650; }
.col h3 i { width:7px; height:7px; border-radius:50%; flex:none; }
.col ul { list-style:none; margin-top:14px; }
.col li { padding:11px 0; border-bottom:1px solid var(--line-soft); font-size:.85rem; color:var(--ink-2); line-height:1.58; }
.col li:last-child { border-bottom:0; }
.col li b { color:var(--ink); font-weight:620; }
/* ---------- precedents ---------- */
.precedents { margin-top:22px; border-top:1px solid var(--line); }
.precedent { padding:19px 0; border-bottom:1px solid var(--line-soft); }
.precedent .top { display:flex; align-items:baseline; gap:11px; flex-wrap:wrap; }
.precedent h3 { font-size:.9rem; font-weight:620; }
.precedent .outcome { font-family:var(--mono); font-size:.66rem; letter-spacing:.07em; color:var(--accent); }
.precedent p { margin-top:7px; font-size:.85rem; color:var(--ink-2); line-height:1.6; max-width:var(--measure); }
.precedent .match { margin-top:8px; font-size:.75rem; color:var(--ink-3); line-height:1.55; }
.precedent .ref { display:inline-block; margin-top:8px; font-size:.7rem; color:var(--ink-3); word-break:break-all; }
.precedent .ref:hover { color:var(--accent); }
/* ---------- reversible path ---------- */
.path { margin-top:22px; counter-reset:step; border-top:1px solid var(--line); }
.step { display:grid; grid-template-columns:30px minmax(0,1fr) auto; gap:16px; padding:19px 0; border-bottom:1px solid var(--line-soft); align-items:start; }
.step .number { counter-increment:step; font-family:var(--mono); font-size:.74rem; color:var(--ink-3); padding-top:2px; }
.step .number::before { content:counter(step,decimal-leading-zero); }
.step h3 { font-size:.9rem; font-weight:620; }
.step p { margin-top:6px; font-size:.83rem; color:var(--ink-2); line-height:1.6; }
.step .cost { font-size:.72rem; color:var(--ink-3); white-space:nowrap; padding-top:3px; }
/* ---------- challenge audit ---------- */
.challenge-grid { display:grid; grid-template-columns:1fr 1fr; gap:44px; margin-top:24px; }
.challenge-col h3 { font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; color:var(--ink-3); font-weight:650; padding-bottom:11px; border-bottom:1px solid var(--line); }
.check { padding:13px 0; border-bottom:1px solid var(--line-soft); font-size:.83rem; color:var(--ink-2); line-height:1.58; }
.check:last-child { border-bottom:0; }
.check .state { margin-right:9px; vertical-align:1px; }
.check b { color:var(--ink); font-weight:620; }
/* ---------- method ---------- */
.method { margin-top:22px; padding-top:22px; border-top:1px solid var(--line); }
.method h3 { font-size:.88rem; font-weight:620; }
.method p { margin-top:10px; font-size:.83rem; color:var(--ink-2); line-height:1.68; max-width:var(--measure); }
.method table { margin-top:24px; min-width:0; }
footer {
margin-top:64px; padding-top:20px; border-top:1px solid var(--line);
color:var(--ink-3); font-size:.73rem; display:flex; gap:20px; flex-wrap:wrap;
}
@media (max-width:960px) { .hero-grid { grid-template-columns:1fr; gap:38px; } }
@media (max-width:760px) {
body { padding:0 18px 64px; font-size:15px; }
.mast { flex-wrap:wrap; gap:10px; }
.mast .meta { margin-left:auto; gap:10px; }
.mast .meta span:not(:first-child) { display:none; }
.cards,.duo,.challenge-grid { grid-template-columns:1fr; gap:30px; }
.lens { grid-template-columns:1fr; gap:9px; }
.step { grid-template-columns:24px minmax(0,1fr); }
.step .cost { grid-column:2; }
section { padding-top:44px; }
}
@media print {
body { padding:0 8mm; font-size:11pt; }
.btn { display:none; }
section { padding-top:26px; break-inside:avoid; }
}
/* ---------- bilingual switch ----------
Both languages ship in the document; the root data-lang attribute selects
one. No new script element is involved: the toggle is the same kind of inline handler
as the theme button. */
:root[data-lang="en"] [data-l="zh"] { display:none; }
:root[data-lang="zh"] [data-l="en"] { display:none; }
:root[data-lang="zh"] body {
font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",var(--sans);
line-height:1.75;
}
:root[data-lang="zh"] .mono,
:root[data-lang="zh"] .scope-word,
:root[data-lang="zh"] .scope-tag,
:root[data-lang="zh"] .outcome,
:root[data-lang="zh"] .step .number { font-family:var(--mono); }
:root[data-lang="zh"] .kicker { letter-spacing:.06em; }
:root[data-lang="zh"] .lens { grid-template-columns:172px minmax(0,1fr); }
</style>
</head>
<body class="v-migrate a-approve">
<div class="wrap">
<header class="mast">
<div class="logo mono">why<span class="q">-not-</span>rust</div>
<div class="meta">
<span class="mono">fish-shell/fish-shell</span><span>2026-08-02</span><span class="mono">method 2.0</span>
<button class="btn" onclick="document.documentElement.dataset.lang=document.documentElement.dataset.lang==='en'?'zh':'en'">EN / 中文</button>
<button class="btn" onclick="document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark'"><span data-l="en">light / dark</span><span data-l="zh">浅色 / 深色</span></button>
</div>
</header>
<section class="hero">
<div class="kicker"><span data-l="en">Rust adoption decision</span><span data-l="zh">Rust 采用决策</span> · <span class="project"><span data-l="en">Rust · interactive shell · 103,667 lines of Rust, 0 C++ files, 2 native survivors</span><span data-l="zh">Rust · 交互式 shell · 103,667 行 Rust,0 个 C++ 文件,2 个原生文件留了下来</span></span></div>
<div class="hero-grid">
<div>
<div class="scope-word">MIGRATE</div>
<div class="scope-sub"><span data-l="en">the one everybody talks about and almost nobody finishes</span><span data-l="zh">所有人都在谈、几乎没人做完的那件事</span></div>
<div class="auth"><i></i><span data-l="en">Authorization</span><span data-l="zh">授权</span> · APPROVE</div>
<p class="why"><span data-l="en">fish is the migration that finished. 78,532 lines of C++ and headers at tag 3.6.0; zero C++ files at HEAD. Deleting the C++ took 338 days and shipping 4.0 took 761, against a plan that guessed six months. The payoff was never speed, and the project says so itself. It was a codebase its own contributors would work in: 96 distinct commit authors in the twelve months before the port, 154 in the twelve months after 4.0.</span><span data-l="zh">fish 是那个真做完了的迁移。3.6.0 这个 tag 上有 78,532 行 C++ 和头文件,HEAD 上一个 C++ 文件都没有。删掉 C++ 用了 338 天,把 4.0 发出去用了 761 天,而当初计划里猜的是半年。买到的东西从来不是速度,项目自己也这么讲。买到的是一份自己人愿意动手的代码:迁移前十二个月有 96 位不同的提交者,4.0 之后的十二个月有 154 位。</span></p>
<p class="trigger"><span data-l="en">Stable. The C++ is deleted and the FFI crates are gone, so nothing reopens the scope question. One promise is still outstanding: concurrent function execution was the reason Rust beat the alternatives in the project's own comparison, and issue 238 has been open since 2012. If it never lands, the language choice was justified by something the project never cashed.</span><span data-l="zh">结论稳定。C++ 已删,FFI crate 也已移除,范围问题不会再打开。有一笔承诺还挂着:并发函数执行是项目自己那份语言对比里 Rust 胜出的理由,而 issue 238 从 2012 年开到现在。如果它一直不落地,当初选语言的那条理由就一直没有兑现。</span></p>
<div class="chips">
<span class="scope"><span data-l="en">Scope</span><span data-l="zh">范围</span> <b><span data-l="en">translate the whole shell to Rust in place, on master, incrementally</span><span data-l="zh">在 master 上就地把整个 shell 增量翻译成 Rust</span></b></span>
<span><span data-l="en">Confidence</span><span data-l="zh">置信度</span> <b>MEDIUM</b></span>
<span><span data-l="en">Robustness</span><span data-l="zh">稳健性</span> <b>STABLE</b></span>
<span><span data-l="en">cli-longtask · interactive shell, forks per job, no network trust boundary</span><span data-l="zh">cli-longtask · 交互式 shell,每个 job 都 fork,没有网络信任边界</span></span>
</div>
</div>
<aside class="gatebox">
<h2><span data-l="en">Four proof gates</span><span data-l="zh">四道证据门</span></h2>
<div class="gate"><span class="id mono">G1</span><div><div class="head"><span class="name"><span data-l="en">Requirement</span><span data-l="zh">需求</span></span><span class="state pass">PASS</span></div><div class="ev"><span data-l="en">17 people ever made 10 or more commits to the C++, across 11 years.</span><span data-l="zh">11 年里,对那份 C++ 提交过 10 次以上的只有 17 个人。</span></div></div></div>
<div class="gate"><span class="id mono">G2</span><div><div class="head"><span class="name"><span data-l="en">Causality</span><span data-l="zh">因果</span></span><span class="state pass">PASS</span></div><div class="ev"><span data-l="en">Send and Sync are compiler-checked. C++ has no equivalent.</span><span data-l="zh">Send 和 Sync 由编译器检查,C++ 里没有对等物。</span></div></div></div>
<div class="gate"><span class="id mono">G3</span><div><div class="head"><span class="name"><span data-l="en">Economics</span><span data-l="zh">经济性</span></span><span class="state pass">PASS</span></div><div class="ev"><span data-l="en">No amount of C++ modernization delivers a different contributor pool.</span><span data-l="zh">把 C++ 现代化到什么程度,都换不来另一批贡献者。</span></div></div></div>
<div class="gate"><span class="id mono">G4</span><div><div class="head"><span class="name"><span data-l="en">Delivery</span><span data-l="zh">交付</span></span><span class="state pass">PASS</span></div><div class="ev"><span data-l="en">166 black-box test files predated the port and outlived it.</span><span data-l="zh">166 个黑盒测试文件在迁移之前就在,迁移之后还在。</span></div></div></div>
</aside>
</div>
</section>
<div class="tiles">
<div class="tile"><div class="label"><span data-l="en">C++ at the last pre-port tag</span><span data-l="zh">迁移前最后一个 tag 上的 C++</span></div><div class="value mono">78,532<span class="unit"><span data-l="en">lines</span><span data-l="zh">行</span></span></div><div class="note"><span data-l="en">3.6.0 · 104 .cpp + 105 .h</span><span data-l="zh">3.6.0 · 104 个 .cpp + 105 个 .h</span></div></div>
<div class="tile"><div class="label"><span data-l="en">C++ files at HEAD</span><span data-l="zh">HEAD 上的 C++ 文件</span></div><div class="value mono">0<span class="unit"><span data-l="en">files</span><span data-l="zh">个</span></span></div><div class="note"><span data-l="en">9654f5e · the whole point of the exercise</span><span data-l="zh">9654f5e · 整件事的重点就在这里</span></div></div>
<div class="tile"><div class="label"><span data-l="en">Proposal to C++ deletion</span><span data-l="zh">从提案到删掉 C++</span></div><div class="value mono">338<span class="unit"><span data-l="en">days</span><span data-l="zh">天</span></span></div><div class="note"><span data-l="en">the plan wrote 'Handwaving, 6 months?'</span><span data-l="zh">计划里写的是「Handwaving, 6 months?」</span></div></div>
<div class="tile"><div class="label"><span data-l="en">Distinct commit authors, 12 months after 4.0</span><span data-l="zh">4.0 之后 12 个月的不同提交者</span></div><div class="value mono">154<span class="unit"><span data-l="en">authors</span><span data-l="zh">位</span></span></div><div class="note"><span data-l="en">96 in the 12 months before the port</span><span data-l="zh">迁移前 12 个月是 96 位</span></div></div>
<div class="tile"><div class="label"><span data-l="en">unsafe occurrences at HEAD</span><span data-l="zh">HEAD 上 unsafe 的出现次数</span></div><div class="value mono">287<span class="unit"><span data-l="en">in 218 .rs files</span><span data-l="zh">分布在 218 个 .rs 文件</span></span></div><div class="note"><span data-l="en">420 at 4.0.0, while the Rust grew 9.5%</span><span data-l="zh">4.0.0 时是 420 次,同期 Rust 代码还多了 9.5%</span></div></div>
<div class="tile"><div class="label"><span data-l="en">Releases between 3.7.1 and 4.0.0</span><span data-l="zh">3.7.1 到 4.0.0 之间的发布</span></div><div class="value mono">0<span class="unit"><span data-l="en">in 345 days</span><span data-l="zh">345 天里</span></span></div><div class="note"><span data-l="en">what the project did not do meanwhile</span><span data-l="zh">这段时间项目没干的事</span></div></div>
</div>
<section>
<h2 class="section-title"><span class="no">01</span><span data-l="en">The options</span><span data-l="zh">备选方案</span></h2>
<p class="sub"><span data-l="en">Every option is judged against the objective the project wrote down before starting: widen the contributor pool, lift the C++11/CMake/ncurses floor, and get compiler-checked thread safety for concurrent functions.</span><span data-l="zh">所有方案都对着项目动手前写下的那个目标:扩大贡献者池,抬掉 C++11/CMake/ncurses 这道地板,并为并发函数拿到编译器检查的线程安全。</span></p>
<div class="table-wrap"><table>
<thead><tr><th><span data-l="en">Option</span><span data-l="zh">方案</span></th><th><span data-l="en">Scope</span><span data-l="zh">范围</span></th><th><span data-l="en">Benefit</span><span data-l="zh">收益</span></th><th><span data-l="en">Cost</span><span data-l="zh">成本</span></th><th><span data-l="en">Time to value</span><span data-l="zh">见效时间</span></th><th><span data-l="en">Compatibility / rollback</span><span data-l="zh">兼容 / 回滚</span></th><th><span data-l="en">Evidence</span><span data-l="zh">证据</span></th></tr></thead>
<tbody>
<tr class="selected"><td><span class="option-name"><span data-l="en">Translate the whole shell to Rust, in place</span><span data-l="zh">就地把整个 shell 翻译成 Rust</span></span><span class="source"><span data-l="en">recommended · the only option that reaches the goal the RFC put first</span><span data-l="zh">推荐 · 唯一打到 RFC 排在第一位那个目标的方案</span></span></td><td><span class="scope-tag">MIGRATE</span></td><td><span data-l="en">class removed from 78,532 lines; contributor count up 60%</span><span data-l="zh">78,532 行里缺陷类消失;贡献者数量涨 60%</span></td><td><span data-l="en">338 days to delete C++; 197 lockfile entries after</span><span data-l="zh">338 天删完 C++;此后 197 条 lockfile 条目</span></td><td><span data-l="en">first Rust in the proposal PR; user-visible at 2 years</span><span data-l="zh">提案 PR 里就有第一批 Rust;用户看得见要 2 年</span></td><td><span data-l="en">black-box harness · C++ built until 2024-01-01</span><span data-l="zh">黑盒测试兜住 · C++ 一直能编到 2024-01-01</span></td><td><span class="ev-tag strong">STRONG</span></td></tr>
<tr class=""><td><span class="option-name"><span data-l="en">Modernize the C++ in place</span><span data-l="zh">在 C++ 里做现代化</span></span><span class="source"><span data-l="en">retain · the funded counterfactual the migration had to beat</span><span data-l="zh">保留 · 迁移必须先赢过的那个已投入对照</span></span></td><td><span class="scope-tag">STAY</span></td><td><span data-l="en">lifts the toolchain floor; contributor pool unchanged</span><span data-l="zh">抬掉工具链地板;贡献者池不变</span></td><td><span data-l="en">months, incremental; nothing new recurring</span><span data-l="zh">数月,可增量做;无新增长期成本</span></td><td><span data-l="en">per release</span><span data-l="zh">随版本发布</span></td><td><span data-l="en">native · no build-system risk</span><span data-l="zh">原生 · 构建系统无风险</span></td><td><span class="ev-tag moderate">MODERATE</span></td></tr>
<tr class=""><td><span class="option-name"><span data-l="en">Ship the C++ concurrency branch as it stands</span><span data-l="zh">直接把那条 C++ 并发分支发出去</span></span><span class="source"><span data-l="en">retain · the smallest option for the goal that is still unmet</span><span data-l="zh">保留 · 针对那个至今未达成目标的最小方案</span></span></td><td><span class="scope-tag">STAY</span></td><td><span data-l="en">delivers issue 238 now; no safety net on thread boundaries</span><span data-l="zh">现在就交付 issue 238;线程边界上没有安全网</span></td><td><span data-l="en">weeks; permanent review burden</span><span data-l="zh">数周;此后每次改动都要人肉审</span></td><td><span data-l="en">one release</span><span data-l="zh">一个发布周期</span></td><td><span data-l="en">semantics change · feature flag</span><span data-l="zh">语义会变 · 用 feature flag 控制</span></td><td><span class="ev-tag moderate">MODERATE</span></td></tr>
<tr class=""><td><span class="option-name"><span data-l="en">Port the leaves only, keep the C++ core</span><span data-l="zh">只把叶子换成 Rust,内核留 C++</span></span><span class="source"><span data-l="en">retain · this was the actual state at month three</span><span data-l="zh">保留 · 第三个月时的真实状态就是这个</span></span></td><td><span class="scope-tag">EXTRACT</span></td><td><span data-l="en">class removed from the leaves; core untouched</span><span data-l="zh">叶子部分缺陷类消失;内核不动</span></td><td><span data-l="en">weeks per component; permanent FFI tax</span><span data-l="zh">每个组件数周;FFI 税永久留下</span></td><td><span data-l="en">weeks</span><span data-l="zh">数周</span></td><td><span data-l="en">same harness · per-component revert</span><span data-l="zh">同一套测试 · 按组件回退</span></td><td><span class="ev-tag strong">STRONG</span></td></tr>
<tr class="excluded"><td><span class="option-name"><span data-l="en">Port to Go instead of Rust</span><span data-l="zh">换成 Go,而不是 Rust</span></span><span class="source"><span data-l="en">exclude · a shell forks for every job</span><span data-l="zh">排除 · shell 每跑一个 job 就 fork 一次</span></span></td><td><span class="scope-tag">MIGRATE</span></td><td><span data-l="en">large contributor pool; fork(2) becomes the problem</span><span data-l="zh">贡献者池很大;fork(2) 变成新问题</span></td><td><span data-l="en">comparable one-time; a GC in the shell</span><span data-l="zh">一次性成本相当;shell 里多一个 GC</span></td><td><span data-l="en">years</span><span data-l="zh">数年</span></td><td><span data-l="en">fork semantics at risk · no clean rollback</span><span data-l="zh">fork 语义有风险 · 没有干净的回退</span></td><td><span class="ev-tag moderate">MODERATE</span></td></tr>
<tr class="excluded"><td><span class="option-name"><span data-l="en">Point users at a shell already written in Rust</span><span data-l="zh">让用户改用已经用 Rust 写的 shell</span></span><span class="source"><span data-l="en">exclude · answers a different question than the one asked</span><span data-l="zh">排除 · 回答的不是这里问的问题</span></span></td><td><span class="scope-tag">ADOPT</span></td><td><span data-l="en">nothing for fish; a different shell for the user</span><span data-l="zh">对 fish 没有收益;对用户是另一个 shell</span></td><td><span data-l="en">per user; a new scripting language</span><span data-l="zh">按用户计;一门新的脚本语言</span></td><td><span data-l="en">immediate for one user</span><span data-l="zh">对单个用户是立刻</span></td><td><span data-l="en">not fish · share/ does not transfer</span><span data-l="zh">不是 fish · share/ 搬不过去</span></td><td><span class="ev-tag strong">STRONG</span></td></tr>
</tbody>
</table></div>
</section>
<!-- OPTIONAL: drop this section in quick mode -->
<section>
<h2 class="section-title"><span class="no">02</span><span data-l="en">Evidence ledger</span><span data-l="zh">证据账本</span></h2>
<p class="sub"><span data-l="en">Each state is evidence scoped to named options, not a score. The performance lenses sit at NEUTRAL or N/A because nobody claimed a performance requirement, including the project. The delivery lenses carry the weight here.</span><span data-l="zh">每条状态都绑定到具体方案,不是可以相加的分数。性能相关的几条停在 NEUTRAL 或 N/A,因为没有人主张过性能需求,项目自己也没有。这里吃重的是交付相关的几条。</span></p>
<div class="ledger">
<div class="lens"><div><div class="name">D1 · <span data-l="en">Requirement & ownership</span><span data-l="zh">需求与归属</span></div><div class="tags"><span class="dir-tag rust">SUPPORTS · rust-full-port, stay-modernize</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">The unmet requirement was people and toolchain, not CPU time. First-party: 17 people made 10 or more commits to the C++ in 11 years. The build floor at 3.6.0 was a C++11 compiler, CMake 3.5 and ncurses. Both halves are owned by the project.</span><span data-l="zh">没被满足的需求是人和工具链,不是 CPU 时间。第一方数据:11 年里对那份 C++ 提交过 10 次以上的有 17 个人。3.6.0 的构建地板是 C++11 编译器、CMake 3.5、ncurses。这两半都在项目自己手里。</span></div><div class="source mono">doc_internal/fish-riir-plan.md:5-8 · README.rst:152 at tag 3.6.0 · https://fishshell.com/blog/rustport/</div></div></div>
<div class="lens"><div><div class="name">D2 · <span data-l="en">End-to-end reach</span><span data-l="zh">端到端影响面</span></div><div class="tags"><span class="dir-tag neutral">N/A</span><span class="ev-tag strong">STRONG</span></div></div><div><div class="claim"><span data-l="en">No Amdahl figure appears here. There was never a performance requirement to build one from. The 4.0.0 notes open with 'there should be no direct impact on users.' The four performance items in that release each carry their own issue number and none is attributed to the language.</span><span data-l="zh">这份报告不给 Amdahl 数字,因为从头到尾没有人提出性能需求。4.0.0 的发布说明开头就写「there should be no direct impact on users」。那个版本里四条性能改进各自挂着 issue 号,没有一条被归给换语言。</span></div><div class="source mono">CHANGELOG.rst:727 · CHANGELOG.rst:841, 882, 961, 962</div></div></div>
<div class="lens"><div><div class="name">D3 · <span data-l="en">Tail & runtime</span><span data-l="zh">尾延迟与运行时</span></div><div class="tags"><span class="dir-tag neutral">NEUTRAL · all</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">There is no collector on either side. Neither C++ nor Rust has one, so no option removes a runtime mechanism from the tail. The project published its own verdict on the outcome: 'usually slightly better in terms of time taken, memory use has a slightly higher floor but a lower ceiling.' That is parity, stated plainly.</span><span data-l="zh">C++ 和 Rust 都没有 collector,所以没有哪个方案能从尾延迟里拿掉运行时机制。项目自己给过结论:'usually slightly better in terms of time taken, memory use has a slightly higher floor but a lower ceiling'。这就是打平,说得很直接。</span></div><div class="source mono">https://fishshell.com/blog/rustport/ · first-party retrospective</div></div></div>
<div class="lens"><div><div class="name">D4 · <span data-l="en">Fleet footprint</span><span data-l="zh">机队占用</span></div><div class="tags"><span class="dir-tag neutral">NEUTRAL · all</span><span class="ev-tag weak">WEAK</span></div></div><div><div class="claim"><span data-l="en">There is no fleet. A shell runs one process per terminal on a developer's own machine, so there is nothing to multiply, no instance price, and no utilization figure. The first-party memory statement cuts both ways: higher floor, lower ceiling.</span><span data-l="zh">shell 是在开发者自己机器上、每个终端一个进程。没有机队可乘,没有实例价格,也没有利用率。第一方那句关于内存的说法是两头都有:floor 更高,ceiling 更低。</span></div><div class="source mono">https://fishshell.com/blog/rustport/</div></div></div>
<div class="lens"><div><div class="name">D5 · <span data-l="en">Startup shape</span><span data-l="zh">启动形态</span></div><div class="tags"><span class="dir-tag neutral">NEUTRAL · all</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">Startup matters a lot here. Every new terminal tab pays it. The lens is neutral because this constraint did its work earlier: it eliminated Java, Python and the scripting family from the RFC's own shortlist. Everything left is a native binary.</span><span data-l="zh">启动在这里很要紧,每开一个终端标签都要付一次。也正因为如此它是中性的:这条约束在 RFC 自己的候选名单里筛掉了 Java、Python 和整个脚本语言家族。活下来的方案全是原生二进制。</span></div><div class="source mono">doc_internal/fish-riir-plan.md:15 · benchmarks/benchmarks/no_execute.fish</div></div></div>
<div class="lens"><div><div class="name">D6 · <span data-l="en">Safety & correctness</span><span data-l="zh">安全与正确性</span></div><div class="tags"><span class="dir-tag rust">SUPPORTS · rust-full-port</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">fish had no memory-safety crisis. Two published advisories exist and both are logic bugs: command-substitution expansion, and a compromised git repository. The port removed the memory-unsafety class from 78,532 lines anyway. What replaced it is a panic that prints 'crashed, please report a bug' and exits.</span><span data-l="zh">fish 没有内存安全危机。公开的安全公告只有两条,而且都是逻辑问题:命令替换的展开,以及一个被污染的 git 仓库。这次迁移还是把 78,532 行里的内存不安全缺陷类拿掉了。取而代之的是一次 panic,打印「crashed, please report a bug」然后退出。</span></div><div class="source mono">gh api repos/fish-shell/fish-shell/security-advisories · src/panic.rs:39</div></div></div>
<div class="lens"><div><div class="name">D7 · <span data-l="en">Concurrency & invariants</span><span data-l="zh">并发与不变量</span></div><div class="tags"><span class="dir-tag rust">SUPPORTS · rust-full-port</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">This is why Rust won the language vote. The maintainer had a working C++ threading branch and would not propose it, because C++ cannot check what crosses a thread boundary. Send and Sync can. First-party: 'the killer feature of Rust, from fish-shell's perspective, is Send and Sync.'</span><span data-l="zh">这是 Rust 在语言投票里胜出的原因。维护者手上有一条能跑的 C++ 线程分支,但一直不肯提出来,因为 C++ 检查不了什么东西跨过了线程边界。Send 和 Sync 能。第一方原话:'the killer feature of Rust, from fish-shell's perspective, is Send and Sync'。</span></div><div class="source mono">doc_internal/fish-riir-plan.md:67-69 · https://fishshell.com/blog/rustport/</div></div></div>
<div class="lens"><div><div class="name">D8 · <span data-l="en">Distribution</span><span data-l="zh">分发</span></div><div class="tags"><span class="dir-tag rust">SUPPORTS · rust-full-port</span><span class="ev-tag strong">STRONG</span></div></div><div><div class="claim"><span data-l="en">The build floor moved and one dependency disappeared. At 3.6.0: a C++11 compiler, CMake 3.5, ncurses headers and libraries. At HEAD: Rust 1.85, CMake 3.15, a C compiler for feature detection, and no curses at all. CMakeLists.txt:5 now declares `project(fish LANGUAGES C)`.</span><span data-l="zh">构建地板移动了,而且少了一个依赖。3.6.0 那时要:C++11 编译器、CMake 3.5、ncurses 的头和库。HEAD 上要:Rust 1.85、CMake 3.15、一个用于特性探测的 C 编译器,curses 完全不需要了。CMakeLists.txt:5 现在写的是 `project(fish LANGUAGES C)`。</span></div><div class="source mono">README.rst:114-121 at HEAD vs README.rst:150-157 at tag 3.6.0 · CMakeLists.txt:5 · CHANGELOG.rst:193, 976</div></div></div>
<div class="lens"><div><div class="name">D9 · <span data-l="en">Ecosystem & alternatives</span><span data-l="zh">生态与替代品</span></div><div class="tags"><span class="dir-tag rust">SUPPORTS · rust-full-port</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">The RFC ran the alternatives before writing any code and recorded why each lost. Java and Python: startup and memory. Go: fork. D, Nim and Zig: too few contributors, higher risk of irrelevance. That is the comparison G2 needs, done in advance rather than reverse-engineered afterwards.</span><span data-l="zh">RFC 在写代码之前就把替代语言过了一遍,并写下每个为什么落选。Java 和 Python:启动和内存。Go:fork。D、Nim、Zig:贡献者太少,语言变得无关的风险更高。G2 要的正是这种对比,而且是事先做的,不是事后倒推的。</span></div><div class="source mono">doc_internal/fish-riir-plan.md:13-18</div></div></div>
<div class="lens"><div><div class="name">D10 · <span data-l="en">Boundary & compatibility</span><span data-l="zh">边界与兼容</span></div><div class="tags"><span class="dir-tag rust">SUPPORTS · rust-full-port, rust-extract-leaf</span><span class="ev-tag strong">STRONG</span></div></div><div><div class="claim"><span data-l="en">A shell's compatibility surface is its observable behaviour, and fish already had it captured. At 3.6.0 the harness was 130 littlecheck files plus 36 pexpect scripts that run the fish binary and diff its output. None of them names an implementation symbol. At HEAD it is 210 and 42.</span><span data-l="zh">shell 的兼容面就是它的可观测行为,而 fish 早就把这部分固定住了。3.6.0 时测试是 130 个 littlecheck 文件加 36 个 pexpect 脚本,跑 fish 二进制然后 diff 输出。没有一个引用实现层的符号。HEAD 上是 210 和 42。</span></div><div class="source mono">tests/checks/ 210 files and tests/pexpects/ 42 files at HEAD; 130 and 36 in the 3.6.0 tarball</div></div></div>
<div class="lens"><div><div class="name">D11 · <span data-l="en">Delivery economics</span><span data-l="zh">交付经济性</span></div><div class="tags"><span class="dir-tag rust">DISFAVORS · rust-full-port</span><span class="ev-tag strong">STRONG</span></div></div><div><div class="claim"><span data-l="en">The plan wrote 'Handwaving, 6 months?' The C++ came out 338 days after the proposal and 4.0.0 shipped 761 days after it. Between 3.7.1 and 4.0.0 there were 345 days with no release of any kind. That is the opportunity cost, and the plan priced none of it.</span><span data-l="zh">计划里写的是「Handwaving, 6 months?」。C++ 在提案后 338 天被删掉,4.0.0 在提案后 761 天才发出去。3.7.1 到 4.0.0 之间有 345 天,一个版本都没发。这就是机会成本,而计划里一分钱也没算。</span></div><div class="source mono">gh api commits (102ab2c90 2024-01-01, 1683e720a 2024-01-12) and releases · doc_internal/fish-riir-plan.md:73</div></div></div>
<div class="lens"><div><div class="name">D12 · <span data-l="en">Counterfactual</span><span data-l="zh">对照方案</span></div><div class="tags"><span class="dir-tag current">SUPPORTS · stay-modernize, stay-concurrent-cpp</span><span class="ev-tag moderate">MODERATE</span></div></div><div><div class="claim"><span data-l="en">Two in-stack routes were cheaper and available. A newer C++ standard lifts the toolchain floor. The threading branch already worked, and shipping it was weeks of work. Each reaches one objective. Neither touches the one the RFC listed first.</span><span data-l="zh">栈内有两条更便宜、而且现成的路。换一个更新的 C++ 标准就能抬掉工具链地板。线程分支本来就能跑,把它发出去是几周的活。它们各能达成一个目标。RFC 排在第一位的那个,两条都碰不到。</span></div><div class="source mono">doc_internal/fish-riir-plan.md:6, 67 · issue 238 open since 2012-07-19</div></div></div>
</div>
<p class="sub"><span data-l="en">One lens is N/A. D2 end-to-end reach: an Amdahl figure needs a performance requirement, and this migration never had one. D3, D4 and D5 sit at NEUTRAL for a related reason — every retained option is a native binary with no collector, so no option removes a runtime mechanism the others keep.</span><span data-l="zh">一条记为 N/A。D2 端到端影响面:算 Amdahl 得先有性能需求,这次迁移从来没有。D3、D4、D5 停在 NEUTRAL 是相近的原因——保留下来的方案全是没有 collector 的原生二进制,谁也没法从尾延迟里拿走别人还留着的运行时机制。</span></p>
</section>
<!-- OPTIONAL: drop this section in quick mode -->
<section>
<h2 class="section-title"><span class="no">03</span><span data-l="en">What decided it</span><span data-l="zh">决定性证据</span></h2>
<div class="cards">
<div class="card rust"><h3><span data-l="en">The C++ really is gone</span><span data-l="zh">C++ 真的没了</span></h3><p><span data-l="en">At tag 3.6.0, three weeks before the proposal, fish carried 78,532 lines of C++ and headers across 209 files. At HEAD the count of .cpp, .cc, .cxx and .hpp files is zero, and no tracked file contains a namespace, a template, or an include of <string>, <vector> or <memory>. Two native files survive: a 100-line Objective-C launcher for the macOS .app bundle, and a 284-line C test helper that exists to misbehave at fish on purpose. Neither is in the shell.</span><span data-l="zh">在提案前三周的 3.6.0 上,fish 有 78,532 行 C++ 和头文件,分布在 209 个文件里。HEAD 上 .cpp、.cc、.cxx、.hpp 的文件数是 0,而且没有任何纳管文件里出现 namespace、template,或者 include <string>/<vector>/<memory>。留下来的原生文件有两个:一个 100 行的 Objective-C 启动器,给 macOS 的 .app 包用;一个 284 行的 C 测试辅助程序,存在的意义就是故意对 fish 使坏。两个都不在 shell 里。</span></p><span class="ref mono">git ls-files | grep -cE '\.(cpp|cc|cxx|hpp)$' → 0 · osx/osx_fish_launcher.m · tests/fish_test_helper.c</span></div>
<div class="card rust"><h3><span data-l="en">The interop scaffolding came out six days after the C++</span><span data-l="zh">FFI 脚手架在 C++ 之后六天就拆了</span></h3><p><span data-l="en">Commit 102ab2c90 removed the FFI code and the C++ files on 2024-01-01. Commit 29bd6eebd removed cxx and autocxx on 2024-01-07. The RFC had said so in advance: 'Once the port is done, we will stop using them.' What remains is a fossil. doc_internal/rust-devel.md still opens with 'fish is in the process of transitioning from C++ to Rust' and describes a fish-rust/ directory that no longer exists.</span><span data-l="zh">commit 102ab2c90 在 2024-01-01 删掉了 FFI 代码和 C++ 文件。commit 29bd6eebd 在 2024-01-07 删掉了 cxx 和 autocxx。RFC 事先就写了这句:「Once the port is done, we will stop using them」。留下来的是一块化石。doc_internal/rust-devel.md 开头还写着「fish is in the process of transitioning from C++ to Rust」,还在介绍一个已经不存在的 fish-rust/ 目录。</span></p><span class="ref mono">gh api commits 102ab2c90, 29bd6eebd · doc_internal/rust-devel.md:7 · doc_internal/fish-riir-plan.md:44</span></div>
<div class="card current"><h3><span data-l="en">The plan said six months; deleting the C++ took 338 days and shipping took 761</span><span data-l="zh">计划说半年;删 C++ 用了 338 天,发布用了 761 天</span></h3><p><span data-l="en">The RFC's timeline section is one sentence: 'Handwaving, 6 months? Frankly unknown - there's 102 remaining .cpp files of various lengths.' The C++ was deleted 338 days after the proposal opened. Shipping took 761. In between, 345 days passed with no release of any kind. Anyone quoting fish as proof that incremental ports are fast should quote the second number too.</span><span data-l="zh">RFC 的时间线只有一句话:「Handwaving, 6 months? Frankly unknown - there's 102 remaining .cpp files of various lengths」。C++ 在提案打开后 338 天被删掉,4.0.0 在 761 天后发出。中间有 345 天,一个版本都没发。谁要拿 fish 当「增量迁移很快」的证据,第二个数字也得一起念。</span></p><span class="ref mono">doc_internal/fish-riir-plan.md:73 · gh api releases: 3.7.1 2024-03-19, 4.0.0 2025-02-27</span></div>
<div class="card rust"><h3><span data-l="en">The test suite predated the port and never changed shape</span><span data-l="zh">测试套件在迁移之前就有,形状一直没变</span></h3><p><span data-l="en">This is the part that transfers. fish's acceptance harness at 3.6.0 was 130 littlecheck files and 36 pexpect scripts, all of which launch the fish binary and compare its output. A language swap cannot invalidate a test that never looked inside. The harness grew during the port rather than being rebuilt, and stands at 210 and 42 today. Compare a project whose tests assert on internal C++ types: that project has to build the harness first.</span><span data-l="zh">能搬走的就是这一块。fish 在 3.6.0 时的验收测试是 130 个 littlecheck 文件和 36 个 pexpect 脚本,全都是起 fish 二进制然后比对输出。一份从不往里看的测试,换语言动不了它。这套测试在迁移期间是长大的,不是重建的,今天是 210 和 42。对照那些断言内部 C++ 类型的项目:那种项目得先把测试重做一遍。</span></p><span class="ref mono">tests/checks/ 210 + tests/pexpects/ 42 at HEAD; 130 + 36 in the 3.6.0 tarball</span></div>
<div class="card unknown"><h3><span data-l="en">Concurrent mode, the reason Rust won the vote, has not shipped</span><span data-l="zh">Rust 胜出的那条理由,并发模式,还没发</span></h3><p><span data-l="en">The RFC's 'Why Rust' section rests on one thing: Send and Sync would let fish turn on concurrent function execution, which the maintainer would not do in C++. Issue 238 was opened 2012-07-19 and is still open. src/builtins/exit.rs:14 carries a TODO saying that in concurrent mode `exit | sleep 1000` may not exit as hoped. The mechanism is in the tree. The feature is not, three and a half years on.</span><span data-l="zh">RFC 的「Why Rust」那一节立在一件事上:Send 和 Sync 能让 fish 打开并发函数执行,而维护者在 C++ 里不肯做这件事。issue 238 在 2012-07-19 打开,至今还开着。src/builtins/exit.rs:14 有一条 TODO,说并发模式下 `exit | sleep 1000` 可能不会按预期退出。机制在树里,功能不在,已经三年半了。</span></p><span class="ref mono">doc_internal/fish-riir-plan.md:67-69 · src/builtins/exit.rs:14 · issue 238, open since 2012-07-19</span></div>
</div>
</section>
<!-- OPTIONAL: drop this section in quick mode -->
<section>
<h2 class="section-title"><span class="no">04</span><span data-l="en">What Rust buys here, and what it doesn't</span><span data-l="zh">Rust 在这里买得到什么,买不到什么</span></h2>
<div class="duo">
<div class="col"><h3><i style="background:var(--rust)"></i><span data-l="en">Buys</span><span data-l="zh">买得到</span></h3><ul>
<li><b><span data-l="en">A codebase its maintainers will work in</span><span data-l="zh">一份维护者愿意动手的代码</span></b> — <span data-l="en">17 people cleared 10 commits on the C++ in 11 years. In the twelve months before the proposal, 96 distinct authors landed commits; in the twelve months after 4.0.0, 154 did. That is the objective the RFC listed first, and the only one with a number attached.</span><span data-l="zh">11 年里,在那份 C++ 上提交过 10 次以上的有 17 个人。提案前十二个月有 96 位不同作者的提交进了树;4.0.0 之后的十二个月是 154 位。这是 RFC 排第一的目标,也是唯一挂得上数字的那个。</span></li><li><b><span data-l="en">The memory-unsafety class, across 78,532 lines, unasked for</span><span data-l="zh">78,532 行里的内存不安全缺陷类,本来没人要</span></b> — <span data-l="en">fish has two published advisories and neither is a memory bug. The class went away anyway. And the unsafe count is falling, not accumulating: 420 occurrences at 4.0.0, 287 at HEAD, while the Rust grew 9.5%.</span><span data-l="zh">fish 公开的安全公告有两条,都不是内存问题。缺陷类还是走了。而且 unsafe 是在减少,不是在堆积:4.0.0 时 420 次,HEAD 上 287 次,同期 Rust 代码还多了 9.5%。</span></li><li><b><span data-l="en">One fewer C dependency to package</span><span data-l="zh">打包时少一个 C 依赖</span></b> — <span data-l="en">ncurses and the terminfo database are both gone by 4.5.0, and CMakeLists.txt:5 now declares LANGUAGES C only. Cygwin came back once rustc gained a Cygwin target.</span><span data-l="zh">到 4.5.0,ncurses 和 terminfo 数据库都不需要了,CMakeLists.txt:5 现在只声明 LANGUAGES C。rustc 有了 Cygwin target 之后,Cygwin 支持也回来了。</span></li><li><b><span data-l="en">A mechanism for the feature that was blocked</span><span data-l="zh">被卡住的那个功能,机制到位了</span></b> — <span data-l="en">Send and Sync are compiler-checked, and the maintainer's stated blocker was that C++ offers nothing equivalent. Whether the feature ships is a separate question, answered below.</span><span data-l="zh">Send 和 Sync 由编译器检查,而维护者明说过卡点就是 C++ 没有对等物。功能到底发不发,是另一个问题,下面回答。</span></li>
</ul></div>
<div class="col"><h3><i style="background:var(--blue)"></i><span data-l="en">Doesn't buy</span><span data-l="zh">买不到</span></h3><ul>
<li><b><span data-l="en">Speed</span><span data-l="zh">速度</span></b> — <span data-l="en">the project's own retrospective calls it parity: 'usually slightly better in terms of time taken, memory use has a slightly higher floor but a lower ceiling.' The 4.0.0 notes open by saying users should notice nothing.</span><span data-l="zh">项目自己的回顾说这是打平:'usually slightly better in terms of time taken, memory use has a slightly higher floor but a lower ceiling'。4.0.0 的发布说明开头就写用户应该察觉不到变化。</span></li><li><b><span data-l="en">Concurrent mode itself</span><span data-l="zh">并发模式本身</span></b> — <span data-l="en">issue 238 has been open since 2012-07-19 and still is. src/builtins/exit.rs:14 has a TODO for it. The tiebreaker that decided the language has not been cashed.</span><span data-l="zh">issue 238 从 2012-07-19 开到现在还开着,src/builtins/exit.rs:14 里为它留着一条 TODO。决定语言的那条理由还没兑现。</span></li><li><b><span data-l="en">Escape from CMake</span><span data-l="zh">从 CMake 里解脱</span></b> — <span data-l="en">the RFC promised 'After porting the C++, we'll replace CMake.' Nine .cmake files plus CMakeLists.txt are still tracked, and README.rst still calls CMake the recommended build.</span><span data-l="zh">RFC 承诺过「After porting the C++, we'll replace CMake」。九个 .cmake 文件加 CMakeLists.txt 还在版本库里,README.rst 也还把 CMake 称为推荐的构建方式。</span></li><li><b><span data-l="en">A crash-free shell</span><span data-l="zh">一个不会崩的 shell</span></b> — <span data-l="en">4.0.1 and 4.0.2 each fixed several crashes. src/panic.rs:39 prints 'crashed, please report a bug' and exits. Rust changed the failure mode from undefined behaviour to a controlled abort, which is worth having and is not the same as no failures.</span><span data-l="zh">4.0.1 和 4.0.2 各修了几个崩溃。src/panic.rs:39 打印「crashed, please report a bug」然后退出。Rust 把失败模式从未定义行为换成了受控中止,这值得有,但跟「不出错」不是一回事。</span></li><li><b><span data-l="en">A binary with no C in it</span><span data-l="zh">一个不含 C 的二进制</span></b> — <span data-l="en">PCRE2 is still C, compiled by pcre2-sys through the cc crate. Every `string match` goes through it.</span><span data-l="zh">PCRE2 还是 C,由 pcre2-sys 通过 cc crate 编译进来。每一次 `string match` 都要走它。</span></li>
</ul></div>
</div>
</section>
<!-- OPTIONAL: drop this section in quick mode -->
<section>
<h2 class="section-title"><span class="no">05</span><span data-l="en">Who has done this before</span><span data-l="zh">同型先例</span></h2>
<div class="precedents">
<article class="precedent"><div class="top"><h3>remacs · Emacs C → Rust</h3><span class="outcome">ABANDONED</span></div><p><span data-l="en">The same strategy, on a target an order of magnitude larger, and it died. remacs translated Emacs primitives to Rust in place, module by module, tests green. Its own README records the high-water mark: '642 functions in Rust and 823 in C (May 2019)' — 44% after two and a half years. Last commit April 2021. The README now opens with 'This project isn't maintained anymore.'</span><span data-l="zh">同一套策略,用在大一个量级的目标上,死了。remacs 就地把 Emacs 的 primitive 一个模块一个模块翻译成 Rust,测试保持绿。它自己的 README 记下了最高水位:'642 functions in Rust and 823 in C (May 2019)'——两年半做到 44%。最后一次提交在 2021 年 4 月。README 现在开头写着'This project isn't maintained anymore'。</span></p><div class="match"><span data-l="en">matches</span><span data-l="zh">匹配</span>: <span data-l="en">identical delivery pattern: in-place incremental port to Rust of a mature C/C++ codebase, on the main line, with an existing test suite</span><span data-l="zh">交付模式完全一样:把成熟的 C/C++ 代码库就地增量移植到 Rust,在主线上做,靠已有测试兜底</span> · <span data-l="en">differs</span><span data-l="zh">不匹配</span>: <span data-l="en">scope, and it settles the comparison. Emacs' src/*.c is 15,511,455 bytes against fish's 2,976,090 of C++, and behind it sit 56,553,319 bytes of Elisp that are the application. fish's 72,635 lines of share/ scripts are data the shell reads, and never entered the port</span><span data-l="zh">范围,而且这是决定性的。Emacs 的 src/*.c 有 15,511,455 字节,fish 的 C++ 是 2,976,090 字节,而 Emacs 背后还压着 56,553,319 字节的 Elisp,那才是应用本体。fish 的 72,635 行 share/ 脚本是 shell 运行时读的数据,从来没进入迁移范围</span></div><a class="ref mono" href="https://github.com/remacs/remacs">first-party · project README</a></article>
<article class="precedent"><div class="top"><h3>Mozilla · Stylo</h3><span class="outcome">MIGRATED</span></div><p><span data-l="en">A C++ CSS engine replaced by Rust inside a shipping Firefox, component by component, both languages in one binary. Two engineers from late 2015, first pixels April 2016, shipped in Firefox 57. Bobby Holley's line is the one fish's RFC could have used: 'Almost everything successful is incremental in one way or another.'</span><span data-l="zh">一个 C++ CSS 引擎在正在发布的 Firefox 里被 Rust 替掉,一个组件一个组件来,两种语言在同一个二进制里。2015 年末两个工程师起手,2016 年 4 月出第一批像素,随 Firefox 57 发布。Bobby Holley 那句话,fish 的 RFC 拿去用也合适:'Almost everything successful is incremental in one way or another'。</span></p><div class="match"><span data-l="en">matches</span><span data-l="zh">匹配</span>: <span data-l="en">in-place incremental C++ to Rust inside a live product, with rollback available at every step</span><span data-l="zh">在一个活着的产品里就地把 C++ 增量换成 Rust,每一步都能回退</span> · <span data-l="en">differs</span><span data-l="zh">不匹配</span>: <span data-l="en">one subsystem, not the whole implementation, and it had a measured performance motive — parallel styling that prior C++ attempts had failed to land safely. fish claimed no performance benefit at all</span><span data-l="zh">那是一个子系统,不是整个实现,而且它有可测量的性能动机——并行 styling,之前几次 C++ 尝试都没能安全落地。fish 完全没有主张性能收益</span></div><a class="ref mono" href="https://bholley.net/blog/2017/stylo.html">first-party · engineering blog</a></article>
<article class="precedent"><div class="top"><h3>curl · the hyper backend</h3><span class="outcome">ABANDONED</span></div><p><span data-l="en">Four years, ISRG funding, near-full test-suite parity, dropped at roughly 95%. Stenberg named the cause and it was not technical: 'There simply were no users asking for it and there were almost no developers interested or knowledgeable enough to work on it.' The rustls and quiche backends survived, because they hooked in more cleanly.</span><span data-l="zh">四年,ISRG 出钱,测试套件接近全过,在大约 95% 的位置被砍掉。Stenberg 点明了原因,而且不是技术原因:'There simply were no users asking for it and there were almost no developers interested or knowledgeable enough to work on it'。rustls 和 quiche 后端活下来了,因为它们接得更干净。</span></p><div class="match"><span data-l="en">matches</span><span data-l="zh">匹配</span>: <span data-l="en">C to Rust, incremental, in place, with no user demand pulling it and a memory-safety rationale nobody had asked for — the same starting position fish had</span><span data-l="zh">C 到 Rust,增量,就地做,没有用户在拉,安全理由也没人要过——fish 的起点是一样的</span> · <span data-l="en">differs</span><span data-l="zh">不匹配</span>: <span data-l="en">one backend of a library rather than a whole implementation, and the outcome inverted: curl could not staff it, while fish's author count rose from 96 to 154</span><span data-l="zh">那是一个库的一个后端,不是整个实现,而结果是反的:curl 招不到人,fish 的作者数从 96 涨到 154</span></div><a class="ref mono" href="https://daniel.haxx.se/blog/2024/12/21/dropping-hyper/">first-party · maintainer blog</a></article>
<article class="precedent"><div class="top"><h3>uutils coreutils</h3><span class="outcome">MIGRATED</span></div><p><span data-l="en">Shipped as Ubuntu 25.10's default with regressions attached: cksum up to 17x slower than GNU on large files, base64 slower before it was faster, and md5sum behaviour differences that broke Makeself installers. The safety rationale still stands. The speed assumption did not.</span><span data-l="zh">作为 Ubuntu 25.10 的默认实现发布,同时带着回归:大文件上 cksum 比 GNU 慢到 17 倍,base64 先慢后快,md5sum 的行为差异弄坏了 Makeself 安装包。安全理由依然成立,速度那个假设不成立。</span></p><div class="match"><span data-l="en">matches</span><span data-l="zh">匹配</span>: <span data-l="en">C to Rust for a userland tool whose contract is its observable behaviour, with no performance case made up front</span><span data-l="zh">把一个用户态工具从 C 换到 Rust,它的契约就是可观测行为,事先也没有立性能理由</span> · <span data-l="en">differs</span><span data-l="zh">不匹配</span>: <span data-l="en">a reimplementation against a spec rather than a translation of the original source. fish translated its own C++ line by line and put fidelity above idiomaticity on purpose</span><span data-l="zh">那是照着规格重新实现,不是把原始代码翻译过来。fish 是逐行翻译自己的 C++,而且刻意把保真放在地道之前</span></div><a class="ref mono" href="https://www.phoronix.com/news/Ubuntu-Rust-Coreutils-Perf">third-party · trade press measurement</a></article>
</div>
</section>
<section>
<h2 class="section-title"><span class="no">06</span><span data-l="en">What to do next</span><span data-l="zh">可逆路径</span></h2>
<div class="path">
<div class="step"><span class="number mono"></span><div><h3><span data-l="en">Count the surface and check the harness before writing any Rust</span><span data-l="zh">动手写 Rust 之前,先数清面,再看测试</span></h3><p><span data-l="en">Inventory the implementation and inspect what the tests actually assert on. fish had 78,532 lines of C++ across 209 files, and an acceptance harness of 130 littlecheck files plus 36 pexpect scripts that launch the binary and diff its output. That second number is the one that decides the project. If the tests assert on internal types, build a behavioural harness first or stop here — a port with no black-box acceptance gate has no way to know it is still correct. Nothing is written at this step.</span><span data-l="zh">把实现清点一遍,再看测试到底断言了什么。fish 有 78,532 行 C++,分布在 209 个文件里;验收测试是 130 个 littlecheck 文件加 36 个 pexpect 脚本,起二进制然后 diff 输出。决定项目命运的是第二个数字。如果测试断言的是内部类型,那就先把行为级测试建起来,或者就停在这里——没有黑盒验收门的迁移,没有办法知道自己还是对的。这一步不写任何代码。</span></p></div><span class="cost"><span data-l="en">1-2 weeks</span><span data-l="zh">1-2 周</span></span></div>
<div class="step"><span class="number mono"></span><div><h3><span data-l="en">Port the leaves on the main branch, both languages in one binary</span><span data-l="zh">在主线上先换叶子,两种语言共存于一个二进制</span></h3><p><span data-l="en">Work on master with no long-lived branch, and keep CI green at every commit. Start with leaf components and move inward: fish's proposal PR shipped FLOG, the topic monitor, wgetopt and builtin_wait, then reached execution in October 2023 and the reader in December. Write Rust that resembles the C++ it replaces, because review and bisect matter more than idiom at this stage. Accept FFI inefficiency only on the understanding that it is temporary. If the bugs start living in the FFI layer rather than in the ported code, stop and keep the hybrid. A permanent hybrid beats a stalled port. Every commit still builds the old implementation.</span><span data-l="zh">在 master 上做,不开长期分支,每个 commit 都保持 CI 绿。从叶子开始往里走:fish 的提案 PR 先交了 FLOG、topic monitor、wgetopt 和 builtin_wait,2023 年 10 月做到 execution,12 月做到 reader。写出来的 Rust 要像它替掉的那段 C++,这个阶段可审可二分比地道更重要。FFI 的低效可以接受,前提是它是临时的。如果 bug 开始长在 FFI 层而不是移植过去的代码里,就停下来保持混合状态——那是一个真实的结果,不是失败。每个 commit 都还能编出旧实现。</span></p></div><span class="cost"><span data-l="en">months, per component</span><span data-l="zh">数月,按组件计</span></span></div>
<div class="step"><span class="number mono"></span><div><h3><span data-l="en">Set the deletion date and hold it</span><span data-l="zh">定下删除日期,然后守住</span></h3><p><span data-l="en">Name the release that removes the old language. Treat that date as a gate. fish put it in the RFC as 'in the span of one release' and hit it. Commit 102ab2c90 removed the FFI code and the C++ on 2024-01-01; 29bd6eebd removed cxx and autocxx six days later. The scaffolding leaves with the old code or it becomes permanent. Budget roughly twice what feels right; fish's own estimate was six months against 338 days. Expect a release freeze too: 345 days passed between 3.7.1 and 4.0.0 with nothing shipped. If the date slips twice, the remaining core is harder than the leaves were and the hybrid is the answer.</span><span data-l="zh">点明哪个版本要移除旧语言,并把它当成一道门。fish 在 RFC 里写的是「in the span of one release」,也守住了:102ab2c90 在 2024-01-01 删掉 FFI 代码和 C++,29bd6eebd 六天后删掉 cxx 和 autocxx。脚手架要么跟旧代码一起走,要么就永久留下。预算大约按感觉的两倍来——fish 自己估半年,实际 338 天——并且要预料到发布停摆:3.7.1 到 4.0.0 之间 345 天没有任何发布。如果日期连滑两次,说明剩下的内核比叶子难得多,答案就是保持混合。</span></p></div><span class="cost"><span data-l="en">1 release cycle; budget 2x the estimate</span><span data-l="zh">一个发布周期;预算按估计的 2 倍</span></span></div>
<div class="step"><span class="number mono"></span><div><h3><span data-l="en">Ship the feature you chose the language for</span><span data-l="zh">把你为之选语言的那个功能发出去</span></h3><p><span data-l="en">A language chosen for one blocked capability is not settled until that capability ships. Set the deadline early. fish chose Rust over Go, Zig and a modernized C++ on one argument: Send and Sync would make concurrent function execution reviewable. Issue 238 was opened in 2012 and is still open, and src/builtins/exit.rs:14 still carries a TODO for it. Set a release deadline for the capability at the same time as the port deadline. Missing it does not undo the other benefits, which for fish landed. It does mean the argument that decided the language was never tested.</span><span data-l="zh">如果选语言的理由是某个被卡住的能力,那这件事在能力发出来之前都没有定论。fish 在 Go、Zig 和现代化 C++ 之间选 Rust,靠的是一条论证:Send 和 Sync 能让并发函数执行变得可审。issue 238 在 2012 年打开,至今还开着,src/builtins/exit.rs:14 里还留着那条 TODO。给这个能力定一个发布期限,和迁移期限一起定。没做到不会抹掉别的收益——fish 拿到的那些是实打实的。但它意味着决定语言的那条论证一直没被检验。</span></p></div><span class="cost"><span data-l="en">1-2 releases after the port</span><span data-l="zh">迁移之后 1-2 个版本</span></span></div>
</div>
</section>
<section>
<h2 class="section-title"><span class="no">07</span><span data-l="en">Arguing both sides</span><span data-l="zh">对称反证</span></h2>
<div class="challenge-grid">
<div class="challenge-col"><h3><span data-l="en">Against migrating</span><span data-l="zh">反驳「迁移」</span></h3>
<div class="check"><span class="state pass">PASS</span><b><span data-l="en">Attribution</span><span data-l="zh">归因</span></b> — <span data-l="en">The project never credited Rust with speed. Its own retrospective reports parity, and the 4.0.0 notes open by telling users to expect no difference. This report does not upgrade that into a performance win.</span><span data-l="zh">项目从没把速度记在 Rust 头上。它自己的回顾说的是打平,4.0.0 的说明开头就叫用户别指望有变化。这份报告没有把那句话升级成性能胜利。</span></div>
<div class="check"><span class="state pass">PASS</span><b><span data-l="en">Baseline and regime</span><span data-l="zh">基线与工况</span></b> — <span data-l="en">The C++ baseline is measured at tag 3.6.0, three weeks before the proposal, not at a convenient later point. The .cpp-only figure excluding fish_tests.cpp is 56,983 lines, which reconciles with the project's own published '57k'.</span><span data-l="zh">C++ 基线量在 3.6.0 这个 tag 上,也就是提案前三周,不是挑一个后面方便的点。去掉 fish_tests.cpp 之后只算 .cpp 是 56,983 行,和项目自己公布的「57k」对得上。</span></div>
<div class="check"><span class="state unknown">HIT</span><b><span data-l="en">Omitted cost</span><span data-l="zh">被省掉的成本</span></b> — <span data-l="en">The RFC priced neither the schedule overrun nor the release freeze. Six months became 338 days to delete the C++ and 761 to ship, with 345 days of no releases in between.</span><span data-l="zh">RFC 既没算进度超支,也没算发布停摆。半年变成了 338 天删完 C++、761 天发出去,中间还有 345 天一个版本都没发。</span></div>
<div class="check"><span class="state pass">PASS</span><b><span data-l="en">Compatibility and dual-run</span><span data-l="zh">兼容与双跑</span></b> — <span data-l="en">Both languages ran in one binary for eleven months, the acceptance harness was black-box and already existed, and the old implementation built at every commit until 102ab2c90.</span><span data-l="zh">两种语言在同一个二进制里跑了十一个月,验收测试是黑盒的而且本来就有,旧实现一直到 102ab2c90 之前每个 commit 都能编。</span></div>
<div class="check"><span class="state unknown">HIT</span><b><span data-l="en">Promise not kept</span><span data-l="zh">没兑现的承诺</span></b> — <span data-l="en">Two of the RFC's own commitments are outstanding. Concurrent mode, the tiebreaker that chose Rust, has not shipped. CMake, which the RFC said would be replaced, is still the recommended build.</span><span data-l="zh">RFC 自己的两条承诺还挂着。并发模式——选 Rust 的那条决胜理由——没有发出来。CMake——RFC 说要替掉的那个——还是推荐的构建方式。</span></div>
</div>
<div class="challenge-col"><h3><span data-l="en">Against staying</span><span data-l="zh">反驳「不动」</span></h3>
<div class="check"><span class="state pass">PASS</span><b><span data-l="en">Funded counterfactual</span><span data-l="zh">有人投入的对照方案</span></b> — <span data-l="en">Two in-stack options are retained and priced: modernize the C++ standard, and ship the threading branch that already worked. Neither is hypothetical.</span><span data-l="zh">两条栈内方案被保留并计了价:把 C++ 标准现代化,以及把那条本来就能跑的线程分支发出去。两条都不是假想。</span></div>
<div class="check"><span class="state unknown">HIT</span><b><span data-l="en">Cost of inaction</span><span data-l="zh">不动的代价</span></b> — <span data-l="en">Staying meant 17 people in 11 years stays the number. For a volunteer project that is the constraint that eventually ends it, and the report does not treat 18 years of shipping C++ as evidence it would keep working.</span><span data-l="zh">不动,就意味着 11 年 17 个人这个数字继续保持。对一个志愿者项目来说,这是最终会把它终结的那条约束;报告没有把 18 年一直在发 C++ 当成它还能继续的证据。</span></div>
<div class="check"><span class="state unknown">HIT</span><b><span data-l="en">Status-quo bias</span><span data-l="zh">现状偏好</span></b> — <span data-l="en">'It compiles and ships' was true of the C++ throughout. It says nothing about whether the next maintainer will show up, which is what the RFC asked about.</span><span data-l="zh">「能编、能发」这句话在整个 C++ 时期都成立。它说不了下一个维护者会不会出现,而 RFC 问的正是这件事。</span></div>
<div class="check"><span class="state pass">PASS</span><b><span data-l="en">Native-advantage denial</span><span data-l="zh">否认原生优势</span></b> — <span data-l="en">The report never claims Rust made fish slower or faster. It records the project's own parity statement and names the profile that would settle it.</span><span data-l="zh">报告既没说 Rust 让 fish 变慢,也没说变快。它记下项目自己那句打平的表述,并点名了能了结这件事的那份 profile。</span></div>
<div class="check"><span class="state pass">PASS</span><b><span data-l="en">Stop condition</span><span data-l="zh">停止条件</span></b> — <span data-l="en">The RFC set a deletion target and the project hit it in 338 days. Compare curl's hyper backend: four years, no date, abandoned at 95%.</span><span data-l="zh">RFC 定了删除目标,项目在 338 天内做到了。对照 curl 的 hyper 后端:四年,没有日期,在 95% 的位置放弃。</span></div>
</div>
</div>
</section>
<section>
<h2 class="section-title"><span class="no">08</span><span data-l="en">How this was produced</span><span data-l="zh">方法与记录</span></h2>
<div class="method"><h3><span data-l="en">fish-shell/fish-shell at 9654f5e · static read-only analysis · why-not-rust method 2.0</span><span data-l="zh">fish-shell/fish-shell @ 9654f5e · 静态只读分析 · why-not-rust 方法 2.0</span></h3><p><span data-l="en">Repository: github.com/fish-shell/fish-shell at commit 9654f5e, shallow clone, 2,205 tracked files. Scope: the whole repository, with the shell implementation as the target. This is a retrospective assessment of a completed migration, so the gates are run against the decision as it stood in January 2023 and the outcome is checked against what the project said it would buy. Sampling: at HEAD, 218 tracked .rs files hold 103,667 lines (89,836 in src/, 13,627 across 21 workspace crates). Zero files match .cpp, .cc, .cxx or .hpp, and no tracked file contains a namespace, template, or an include of <string>, <vector> or <memory>. Two native files remain: osx/osx_fish_launcher.m at 100 lines, the launcher stub for the macOS .app bundle, and tests/fish_test_helper.c at 284 lines, a dependency-free program that mimics awkward child processes so fish can test job control against it. At 4.0.0 there were three, including a 182-line src/libc.c that has since gone. Counting bases are stated wherever a comparison appears and are never mixed. The pre-port C++ baseline is 78,532 lines across 209 files at tag 3.6.0, split 64,328 lines in 104 .cpp files and 14,204 in 105 .h files; excluding the 7,345-line fish_tests.cpp gives 56,983, which reconciles with the project's own published '57k'. Rust at tag 4.0.0 was 94,638 lines in 194 files, so the growth to HEAD is 9.5%. Over the same span `unsafe` occurrences fell from 420 to 287 across 58 of 218 files. The scripting layer was never in scope: 72,635 lines of .fish under share/ are data the shell reads at runtime, and that is the structural difference from remacs, where 56,553,319 bytes of Elisp sit behind the C being ported. Timeline from the GitHub API: PR 9512 opened 2023-01-28; commit 102ab2c90 removed the FFI code and the C++ on 2024-01-01, 338 days later; 29bd6eebd removed cxx and autocxx on 2024-01-07; 4.0.0 shipped 2025-02-27, 761 days after the proposal, against an RFC estimate of 'Handwaving, 6 months?'. Between 3.7.1 (2024-03-19) and 4.0.0 there were 345 days with no release. Distinct commit authors: 96 in the twelve months to 2023-01-28, 154 in the twelve months from 2025-02-27. Objective: taken from doc_internal/fish-riir-plan.md, the project's own RFC, since no separate requirements document exists. User-supplied facts: none. No Amdahl calculation appears, because no performance requirement was ever stated and substituting a line-count share for a time share would be a method error. The decision turns on G1 and G4, both on directly measured facts; G3 passes on a structural argument, since no in-stack option changes which language contributors are asked to write. Confidence is MEDIUM rather than HIGH because the benefit that justified the work is corroborated by a correlational author count, and because concurrent function execution — the argument that selected Rust over the alternatives — has still not shipped. This is a structured decision protocol, not a statistical predictor.</span><span data-l="zh">仓库:github.com/fish-shell/fish-shell,commit 9654f5e,shallow clone,2,205 个纳管文件。范围:整个仓库,评估目标是 shell 实现本体。这是对一次已完成迁移的回溯评估,所以四道门是按 2023 年 1 月那个时点的决策来跑的,结果则拿项目自己说要买到的东西来对。采样:HEAD 上 218 个纳管 .rs 文件共 103,667 行(src/ 里 89,836 行,21 个 workspace crate 里 13,627 行)。匹配 .cpp、.cc、.cxx、.hpp 的文件数为 0,并且没有任何纳管文件里出现 namespace、template,或 include <string>/<vector>/<memory>。留下两个原生文件:osx/osx_fish_launcher.m,100 行,macOS .app 包的启动壳;tests/fish_test_helper.c,284 行,一个不依赖 fish 的小程序,专门装成各种别扭的子进程,好让 fish 拿它测作业控制。4.0.0 时还有第三个,182 行的 src/libc.c,后来也没了。凡是出现对比的地方都写明口径,绝不混用。迁移前的 C++ 基线是 3.6.0 这个 tag 上 209 个文件、78,532 行,其中 104 个 .cpp 共 64,328 行,105 个 .h 共 14,204 行;扣掉 7,345 行的 fish_tests.cpp 得到 56,983 行,和项目自己公布的「57k」对得上。4.0.0 上的 Rust 是 194 个文件、94,638 行,到 HEAD 增长 9.5%。同一区间里,`unsafe` 出现次数从 420 降到 287,分布在 218 个文件中的 58 个。脚本层从来不在范围内:share/ 下 72,635 行 .fish 是 shell 运行时读取的数据,这正是和 remacs 的结构性差别——remacs 要移植的那些 C 后面压着 56,553,319 字节的 Elisp。时间线取自 GitHub API:PR 9512 在 2023-01-28 打开;commit 102ab2c90 在 2024-01-01 删掉 FFI 代码和 C++,距提案 338 天;29bd6eebd 在 2024-01-07 删掉 cxx 和 autocxx;4.0.0 在 2025-02-27 发布,距提案 761 天,而 RFC 的估计是「Handwaving, 6 months?」。3.7.1(2024-03-19)到 4.0.0 之间有 345 天没有任何发布。不同提交作者数:到 2023-01-28 的十二个月里 96 位,从 2025-02-27 起的十二个月里 154 位。目标:取自 doc_internal/fish-riir-plan.md,也就是项目自己的 RFC,因为没有单独的需求文档。用户提供的事实:无。本报告没有 Amdahl 计算,因为从头到尾没有人提出性能需求,而拿代码行数占比顶替时间占比是方法错误。决策落在 G1 和 G4 上,两道门都基于直接测量的事实;G3 通过靠的是结构性论证——没有任何栈内方案能改变「请贡献者写哪门语言」这件事。置信度给 MEDIUM 而不是 HIGH,一是支撑这笔投入的收益只有一个相关性的作者数在佐证,二是并发函数执行——那条让 Rust 赢过其他候选的论证——至今没有发出来。这是一套结构化决策流程,不是统计预测器。</span></p>
<div class="table-wrap"><table>
<thead><tr><th><span data-l="en">What we don't know</span><span data-l="zh">证据缺口</span></th><th><span data-l="en">What it would change</span><span data-l="zh">会改变什么</span></th></tr></thead>
<tbody><tr><td><span data-l="en">A published before-and-after profile of shell startup and keystroke latency</span><span data-l="zh">一份公开的 shell 启动与按键延迟的前后对比 profile</span></td><td><span data-l="en">The parity claim is a maintainer's prose summary, not an artifact. The repository ships a benchmark harness with 15 cases and commits no results. While that is missing, D3 stays MODERATE and D4 stays WEAK.</span><span data-l="zh">打平这个说法是维护者的文字总结,不是可核查的产物。仓库里有一套 15 个用例的基准脚手架,但没有提交任何结果。缺着它,D3 停在 MODERATE,D4 停在 WEAK。</span></td></tr><tr><td><span data-l="en">Causal evidence linking the author count to the language change</span><span data-l="zh">把作者数变化和换语言联系起来的因果证据</span></td><td><span data-l="en">96 to 154 distinct authors across matched twelve-month windows is the only number attached to the RFC's first objective, and it is correlation. Someone contributing a completion script is unlikely to care what the shell is written in.</span><span data-l="zh">在两个对齐的十二个月窗口里,不同作者从 96 到 154,是 RFC 第一个目标唯一挂得上的数字,而它是相关性。提交一个补全脚本的人,大概不在意 shell 是用什么写的。</span></td></tr><tr><td><span data-l="en">Whether CI was actually green at every commit through the port</span><span data-l="zh">迁移期间 CI 是否真的每个 commit 都绿</span></td><td><span data-l="en">The RFC required it and the project asserts it. Run-level workflow history was not checked here. It is the load-bearing G4 claim, so a reader who cares about the delivery lesson should verify it rather than take it from this report.</span><span data-l="zh">RFC 要求过,项目也这么说。这里没有去查 workflow 的逐次运行记录。这是 G4 里吃重的一条,真在意交付这条经验的读者应该自己去核,而不是从这份报告里拿。</span></td></tr></tbody>
</table></div>
</div>
</section>
<footer><span>why-not-rust · method 2.0 · <span data-l="en">public repository · static analysis at commit 9654f5e · baselines from gh api tarballs · no build, benchmark or network call against the target</span><span data-l="zh">公开仓库 · 在 commit 9654f5e 上做静态分析 · 基线取自 gh api 下载的源码包 · 没有对目标做构建、基准或网络调用</span></span><span class="mono">github.com/xiaotonng/why-not-rust</span></footer>
</div>
<script type="application/json" id="why-not-rust-assessment">
{
"method": "why-not-rust/2.0",
"generated_at": "2026-08-02",
"repository": {
"path": "https://github.com/fish-shell/fish-shell",
"commit": "9654f5e4bd00066e8d0db7fdb66e7b12458f8f4e",
"scope": "whole repository; the shell implementation is the assessed target",
"sampling": "shallow clone at 9654f5e, 2,205 tracked files enumerated; src/, crates/, tests/, share/, cmake/ and doc_internal/ measured; the pre-port baseline and the 4.0.0 snapshot from source tarballs fetched via gh api; history, releases, advisories and author counts from the GitHub API; no build, test or benchmark was run"
},
"analysis": {
"mode": "public-repository static analysis; no build, benchmark, or network run against the target",
"user_supplied_facts": [],
"evidence_gaps": [
"A published before-and-after profile of shell startup and keystroke latency",
"Causal evidence linking the author count to the language change",
"Whether CI was actually green at every commit through the port"
]
},
"objective": {
"driver": "maintainability and contributor supply",
"requirement": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"baseline": "78,532 lines of C++ and headers across 209 files at tag 3.6.0; 17 people had made 10 or more commits to that code in 11 years; no memory-safety advisory exists in fish's published history",
"target": "zero C++ in the shell, tests green at every commit, delivered within one release cycle"
},
"decision": {
"authorization": "APPROVE",
"scope": "MIGRATE",
"selected_option_id": "rust-full-port",
"confidence": "MEDIUM",
"robustness": "STABLE",
"because": "fish is the migration that finished. 78,532 lines of C++ and headers at tag 3.6.0; zero C++ files at HEAD. Deleting the C++ took 338 days and shipping 4.0 took 761, against a plan that guessed six months. The payoff was never speed, and the project says so itself. It was a codebase its own contributors would work in: 96 distinct commit authors in the twelve months before the port, 154 in the twelve months after 4.0.",
"change_trigger": "Stable. The C++ is deleted and the FFI crates are gone, so nothing reopens the scope question. One promise is still outstanding: concurrent function execution was the reason Rust beat the alternatives in the project's own comparison, and issue 238 has been open since 2012. If it never lands, the language choice was justified by something the project never cashed."
},
"gates": [
{
"id": "G1",
"option_id": "rust-full-port",
"name": "requirement",
"state": "PASS",
"evidence": "The gap was contributor supply and a toolchain floor, both named in the project's own RFC at doc_internal/fish-riir-plan.md:5-8. First-party: only 17 people had 10 or more commits to the C++ code in 11 years. The C++ build floor at tag 3.6.0 was a C++11 compiler, CMake 3.5 and ncurses headers. No memory-safety advisory exists in fish's published history, and the RFC does not claim one."
},
{
"id": "G2",
"option_id": "rust-full-port",
"name": "rust-specific causality",
"state": "PASS",
"evidence": "The RFC ran the alternatives before the work started and wrote down why each lost: Java and Python on startup and memory, Go on fork(2), and D/Nim/Zig on contributor supply (doc_internal/fish-riir-plan.md:15-17). The Rust-specific mechanism is compiler-checked thread safety: the maintainer had a working C++ threading branch and would not propose it because C++ cannot enforce what crosses a thread boundary (doc_internal/fish-riir-plan.md:67-69)."
},
{
"id": "G3",
"option_id": "rust-full-port",
"name": "economics and smallest sufficient option",
"state": "PASS",
"evidence": "Two cheaper in-stack options existed and are retained: modernize the C++ standard, and ship the C++ threading branch that already worked. Each reaches one stated goal. Neither reaches the goal the RFC put first, because the language itself was the barrier to entry. Cost is not hidden: 338 days to delete the C++, 761 to ship 4.0.0, and 345 days with no release of any kind between 3.7.1 and 4.0.0."
},
{
"id": "G4",
"option_id": "rust-full-port",
"name": "delivery and reversibility",
"state": "PASS",
"evidence": "At tag 3.6.0 the acceptance harness was 130 littlecheck files plus 36 pexpect scripts that drive the fish binary and diff its output; none references an implementation symbol. Work stayed on master with both languages in one binary via forked cxx/autocxx plus corrosion, ordered outside-in from builtins to reader. Rollback was live at every commit until 2024-01-01, when 102ab2c90 removed the C++."
}
],
"options": [
{
"id": "rust-full-port",
"name": "Translate the whole shell to Rust, in place",
"implementation": "rust",
"scope": "full",
"target": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"benefit_interval": "class removed from 78,532 lines; contributor count up 60%",
"one_time_cost": "338 days to delete the C++; 761 to ship 4.0.0",
"recurring_cost": "197 Cargo.lock entries; a Rust toolchain in every distro build",
"time_to_value": "first Rust in the proposal PR; user-visible at 2 years",
"compatibility": "shell behaviour, captured by a pre-existing black-box harness",
"reversibility": "git revert, at any commit before 102ab2c90",
"evidence_strength": "STRONG",
"disposition": "selected",
"reason": "The RFC's first objective was contributor supply, and the language was the barrier. No in-stack change reaches that. Cost overran the plan by 1.9x on code and 4.2x on release, and it was still paid and finished."
},
{
"id": "stay-modernize",
"name": "Modernize the C++ in place",
"implementation": "current",
"scope": "stay",
"target": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"benefit_interval": "lifts the toolchain floor; contributor pool unchanged",
"one_time_cost": "months, incremental",
"recurring_cost": "none new",
"time_to_value": "per release",
"compatibility": "native",
"reversibility": "git revert",
"evidence_strength": "MODERATE",
"disposition": "retain",
"reason": "Reaches the toolchain half of the objective for a fraction of the cost. It leaves the project writing C++, which is the thing the RFC said was driving contributors away."
},
{
"id": "stay-concurrent-cpp",
"name": "Ship the C++ concurrency branch as it stands",
"implementation": "current",
"scope": "stay",
"target": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"benefit_interval": "delivers issue 238 now; no safety net on thread boundaries",
"one_time_cost": "weeks; the branch already worked",
"recurring_cost": "review burden on every threaded change",
"time_to_value": "one release",
"compatibility": "changes observable execution semantics",
"reversibility": "feature flag",
"evidence_strength": "MODERATE",
"disposition": "retain",
"reason": "The cheapest path to the concurrency goal, and three and a half years later it is still the only path that has actually been tried. The maintainer's objection was reviewability, not feasibility."
},
{
"id": "rust-extract-leaf",
"name": "Port the leaves only, keep the C++ core",
"implementation": "rust",
"scope": "extract",
"target": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"benefit_interval": "class removed from the leaves; core untouched",
"one_time_cost": "weeks per component",
"recurring_cost": "cxx/autocxx maintained forever",
"time_to_value": "weeks",
"compatibility": "same harness",
"reversibility": "per component",
"evidence_strength": "STRONG",
"disposition": "retain",
"reason": "The natural place to stop, and the RFC priced FFI string copying as acceptable only because it was temporary. Stopping here converts that into a permanent tax and reaches none of the three goals."
},
{
"id": "native-go",
"name": "Port to Go instead of Rust",
"implementation": "external",
"scope": "full",
"target": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"benefit_interval": "large contributor pool; fork(2) becomes the problem",
"one_time_cost": "comparable to the Rust port",
"recurring_cost": "a GC in a per-keystroke process",
"time_to_value": "years",
"compatibility": "job control depends on fork semantics",
"reversibility": "none",
"evidence_strength": "MODERATE",
"disposition": "exclude",
"reason": "fish forks and then does signal, pgroup and tty setup in the child. The RFC excluded Go on exactly that ground, and it also gives up the compiler-checked thread safety that was the tiebreaker."
},
{
"id": "adopt-other-shell",
"name": "Point users at a shell already written in Rust",
"implementation": "external",
"scope": "adopt",
"target": "move fish's implementation off C++ to widen the contributor pool, lift the C++11/CMake/ncurses toolchain floor, and obtain compiler-checked thread safety for concurrent function execution",
"benefit_interval": "nothing for fish; a different shell for the user",
"one_time_cost": "per user",
"recurring_cost": "relearning the scripting language",
"time_to_value": "immediate for one user",
"compatibility": "not fish; 72,635 lines of share/ scripts do not transfer",
"reversibility": "switch back",
"evidence_strength": "STRONG",
"disposition": "exclude",
"reason": "Retained because the method requires an adoption option, and excluded because fish's objective was about fish's maintainers. A user switching shells changes nothing for the project."
}
],
"lenses": [
{
"id": "D1",
"name": "Requirement \u0026 ownership",
"option_ids": [
"rust-full-port",
"stay-modernize",
"rust-extract-leaf"
],
"state": "SUPPORTS",
"strength": "MODERATE",
"claim": "The unmet requirement was people and toolchain, not CPU time. First-party: 17 people made 10 or more commits to the C++ in 11 years. The build floor at 3.6.0 was a C++11 compiler, CMake 3.5 and ncurses. Both halves are owned by the project.",
"source": "doc_internal/fish-riir-plan.md:5-8 · README.rst:152 at tag 3.6.0 · https://fishshell.com/blog/rustport/",
"baseline_regime": "the project's own RFC plus a first-party retrospective",
"caveat": "The toolchain half is also reachable by stay-modernize, so D1 alone does not make this a Rust-specific finding. Only D7 and D9 do that.",
"change_trigger": "A contributor count that had risen under C++ would have removed the first objective entirely."
},
{
"id": "D2",
"name": "End-to-end reach",
"option_ids": [],
"state": "N/A",
"strength": "STRONG",
"claim": "No Amdahl figure appears here. There was never a performance requirement to build one from. The 4.0.0 notes open with 'there should be no direct impact on users.' The four performance items in that release each carry their own issue number and none is attributed to the language.",
"source": "CHANGELOG.rst:727 · CHANGELOG.rst:841, 882, 961, 962",
"baseline_regime": "n/a — no performance target exists",
"caveat": "Substituting a line-count share for a time share would be a method error, and there is no profile to substitute anyway.",
"change_trigger": ""
},
{
"id": "D3",
"name": "Tail \u0026 runtime",
"option_ids": [],
"state": "NEUTRAL",
"strength": "MODERATE",
"claim": "There is no collector on either side. Neither C++ nor Rust has one, so no option removes a runtime mechanism from the tail. The project published its own verdict on the outcome: 'usually slightly better in terms of time taken, memory use has a slightly higher floor but a lower ceiling.' That is parity, stated plainly.",
"source": "https://fishshell.com/blog/rustport/ · first-party retrospective",
"baseline_regime": "first-party prose, no published measurement method",
"caveat": "This is a maintainer's summary, not a benchmark artifact. It is quoted as evidence of what the project claims, not as a measurement.",
"change_trigger": "A published before/after profile of shell startup and keystroke latency would move this to STRONG in either direction."
},
{
"id": "D4",
"name": "Fleet footprint",
"option_ids": [],
"state": "NEUTRAL",
"strength": "WEAK",
"claim": "There is no fleet. A shell runs one process per terminal on a developer's own machine, so there is nothing to multiply, no instance price, and no utilization figure. The first-party memory statement cuts both ways: higher floor, lower ceiling.",
"source": "https://fishshell.com/blog/rustport/",
"baseline_regime": "n/a — no fleet arithmetic applies",
"caveat": "A higher memory floor is a regression for someone running hundreds of shells; nobody has measured that case.",
"change_trigger": ""
},
{
"id": "D5",
"name": "Startup shape",
"option_ids": [],
"state": "NEUTRAL",
"strength": "MODERATE",
"claim": "Startup matters a lot here. Every new terminal tab pays it. The lens is neutral because this constraint did its work earlier: it eliminated Java, Python and the scripting family from the RFC's own shortlist. Everything left is a native binary.",
"source": "doc_internal/fish-riir-plan.md:15 · benchmarks/benchmarks/no_execute.fish",
"baseline_regime": "structural constraint, applied at language-selection time",
"caveat": "The repository ships a benchmark harness at benchmarks/driver.sh with 15 cases, but no before/after results are committed.",
"change_trigger": ""
},
{
"id": "D6",
"name": "Safety \u0026 correctness",
"option_ids": [
"rust-full-port",
"rust-extract-leaf"
],
"state": "SUPPORTS",
"strength": "MODERATE",
"claim": "fish had no memory-safety crisis. Two published advisories exist and both are logic bugs: command-substitution expansion, and a compromised git repository. The port removed the memory-unsafety class from 78,532 lines anyway. What replaced it is a panic that prints 'crashed, please report a bug' and exits.",
"source": "gh api repos/fish-shell/fish-shell/security-advisories · src/panic.rs:39",
"baseline_regime": "structural, at the whole-implementation scope",
"caveat": "PCRE2 is still C. `string match` runs through pcre2-sys, which compiles the C library with the cc crate (Cargo.lock:982). And 4.0.1 and 4.0.2 each fixed several crashes — Rust changed the failure mode, not the existence of failures.",
"change_trigger": "A memory-safety advisory in the C++ era would have made this the leading objective instead of a side effect."
},
{
"id": "D7",
"name": "Concurrency \u0026 invariants",
"option_ids": [
"rust-full-port"
],
"state": "SUPPORTS",
"strength": "MODERATE",
"claim": "This is why Rust won the language vote. The maintainer had a working C++ threading branch and would not propose it, because C++ cannot check what crosses a thread boundary. Send and Sync can. First-party: 'the killer feature of Rust, from fish-shell's perspective, is Send and Sync.'",
"source": "doc_internal/fish-riir-plan.md:67-69 · https://fishshell.com/blog/rustport/",
"baseline_regime": "design argument, first-party, from the maintainer who wrote both branches",
"caveat": "The mechanism is in place and the feature is not. Issue 238 has been open since 2012-07-19, and src/builtins/exit.rs:14 still carries a TODO for concurrent mode. Three and a half years after the proposal, this is the one objective with nothing shipped against it.",
"change_trigger": "Concurrent function execution landing in a release would move this to STRONG."
},
{
"id": "D8",
"name": "Distribution",
"option_ids": [
"rust-full-port"
],
"state": "SUPPORTS",
"strength": "STRONG",
"claim": "The build floor moved and one dependency disappeared. At 3.6.0: a C++11 compiler, CMake 3.5, ncurses headers and libraries. At HEAD: Rust 1.85, CMake 3.15, a C compiler for feature detection, and no curses at all. CMakeLists.txt:5 now declares `project(fish LANGUAGES C)`.",
"source": "README.rst:114-121 at HEAD vs README.rst:150-157 at tag 3.6.0 · CMakeLists.txt:5 · CHANGELOG.rst:193, 976",
"baseline_regime": "static build-manifest comparison across two tagged trees",
"caveat": "The RFC promised 'After porting the C++, we'll replace CMake' at line 36. Nine .cmake files and CMakeLists.txt are still tracked, and README.rst still calls CMake the recommended path. Distributors traded a C++ toolchain for a Rust one plus 197 Cargo.lock entries.",
"change_trigger": "Cargo becoming the sole supported build would close the second half of this objective."
},
{
"id": "D9",
"name": "Ecosystem \u0026 alternatives",
"option_ids": [
"rust-full-port",
"native-go"
],
"state": "SUPPORTS",
"strength": "MODERATE",
"claim": "The RFC ran the alternatives before writing any code and recorded why each lost. Java and Python: startup and memory. Go: fork. D, Nim and Zig: too few contributors, higher risk of irrelevance. That is the comparison G2 needs, done in advance rather than reverse-engineered afterwards.",
"source": "doc_internal/fish-riir-plan.md:13-18",
"baseline_regime": "the project's own written language selection",
"caveat": "The contributor half of the argument is a forecast about which language attracts people. It could not be verified in advance, and the 96-to-154 movement afterwards is correlation, not proof.",
"change_trigger": "A shell-shaped Go project solving the fork problem cleanly would weaken the exclusion of native-go."
},
{
"id": "D10",
"name": "Boundary \u0026 compatibility",
"option_ids": [
"rust-full-port",
"rust-extract-leaf"
],
"state": "SUPPORTS",
"strength": "STRONG",
"claim": "A shell's compatibility surface is its observable behaviour, and fish already had it captured. At 3.6.0 the harness was 130 littlecheck files plus 36 pexpect scripts that run the fish binary and diff its output. None of them names an implementation symbol. At HEAD it is 210 and 42.",
"source": "tests/checks/ 210 files and tests/pexpects/ 42 files at HEAD; 130 and 36 in the 3.6.0 tarball",
"baseline_regime": "file inventory across two tagged trees",
"caveat": "fish's other large body of code never entered scope: 72,635 lines of .fish scripts under share/ are data the shell reads at runtime. That is a property of shells, and it is the single biggest structural difference from remacs.",
"change_trigger": "A harness that asserted on C++ internals would have had to be rebuilt first, and G4 would not have passed on day one."
},
{
"id": "D11",
"name": "Delivery economics",
"option_ids": [
"rust-full-port"
],
"state": "DISFAVORS",
"strength": "STRONG",
"claim": "The plan wrote 'Handwaving, 6 months?' The C++ came out 338 days after the proposal and 4.0.0 shipped 761 days after it. Between 3.7.1 and 4.0.0 there were 345 days with no release of any kind. That is the opportunity cost, and the plan priced none of it.",
"source": "gh api commits (102ab2c90 2024-01-01, 1683e720a 2024-01-12) and releases · doc_internal/fish-riir-plan.md:73",
"baseline_regime": "commit and release dates from the GitHub API",
"caveat": "The 3.7 maintenance branch kept shipping through March 2024, so users were not stranded. The 1.9x code overrun sits inside the Crosslake rewrite dataset's 1.5-2x band, which makes it typical rather than exceptional.",
"change_trigger": "Nothing reopens this; the cost is spent."
},
{
"id": "D12",
"name": "Counterfactual",
"option_ids": [
"stay-modernize",
"stay-concurrent-cpp"
],
"state": "SUPPORTS",
"strength": "MODERATE",
"claim": "Two in-stack routes were cheaper and available. A newer C++ standard lifts the toolchain floor. The threading branch already worked, and shipping it was weeks of work. Each reaches one objective. Neither touches the one the RFC listed first.",
"source": "doc_internal/fish-riir-plan.md:6, 67 · issue 238 open since 2012-07-19",
"baseline_regime": "the RFC's own description of the alternatives it declined",
"caveat": "The concurrency objective is still unmet under the migration too, which means stay-concurrent-cpp is the only option whose target has been reached by nobody.",
"change_trigger": "If concurrent mode never ships, this lens becomes the strongest argument that the tiebreaker was never cashed."
}
],
"math": {
"amdahl": null,
"break_even": null
},
"precedents": [
{
"name": "remacs · Emacs C → Rust",
"outcome": "ABANDONED",
"match": "identical delivery pattern: in-place incremental port to Rust of a mature C/C++ codebase, on the main line, with an existing test suite",
"mismatch": "scope, and it settles the comparison. Emacs' src/*.c is 15,511,455 bytes against fish's 2,976,090 of C++, and behind it sit 56,553,319 bytes of Elisp that are the application. fish's 72,635 lines of share/ scripts are data the shell reads, and never entered the port",
"workload_regime": "file inventory via gh api git/trees at each project's master",
"source_class": "first-party · project README",
"url": "https://github.com/remacs/remacs"
},
{
"name": "Mozilla · Stylo",
"outcome": "MIGRATED",
"match": "in-place incremental C++ to Rust inside a live product, with rollback available at every step",
"mismatch": "one subsystem, not the whole implementation, and it had a measured performance motive — parallel styling that prior C++ attempts had failed to land safely. fish claimed no performance benefit at all",
"workload_regime": "shipped in Firefox 57, Nov 2017, first-party",
"source_class": "first-party · engineering blog",
"url": "https://bholley.net/blog/2017/stylo.html"
},
{
"name": "curl · the hyper backend",
"outcome": "ABANDONED",
"match": "C to Rust, incremental, in place, with no user demand pulling it and a memory-safety rationale nobody had asked for — the same starting position fish had",
"mismatch": "one backend of a library rather than a whole implementation, and the outcome inverted: curl could not staff it, while fish's author count rose from 96 to 154",
"workload_regime": "removed in curl 8.12.0, Feb 2025; drop announced Dec 21, 2024",
"source_class": "first-party · maintainer blog",
"url": "https://daniel.haxx.se/blog/2024/12/21/dropping-hyper/"
},
{
"name": "uutils coreutils",
"outcome": "MIGRATED",
"match": "C to Rust for a userland tool whose contract is its observable behaviour, with no performance case made up front",
"mismatch": "a reimplementation against a spec rather than a translation of the original source. fish translated its own C++ line by line and put fidelity above idiomaticity on purpose",
"workload_regime": "Ubuntu 25.10 default, third-party measurement",
"source_class": "third-party · trade press measurement",
"url": "https://www.phoronix.com/news/Ubuntu-Rust-Coreutils-Perf"
}
],
"path": [
{
"step": 1,
"title": "Count the surface and check the harness before writing any Rust",
"owner": "the maintainer proposing the port",
"cost_range": "1-2 weeks",
"artifact": "a file and line inventory of the implementation, plus a written finding on whether the existing test suite asserts on observable behaviour or on internal types",
"acceptance": "the harness runs the shipped binary and compares output only, with no reference to implementation symbols",
"deadline_or_stop": "stop, or fund a behavioural harness first, if the tests assert on internal types",
"rollback": "measurement only; no code changes"
},
{
"step": 2,
"title": "Port the leaves on the main branch, both languages in one binary",
"owner": "the porting contributors",
"cost_range": "months, per component",
"artifact": "leaf components in Rust, linked into the existing binary through an FFI layer that is explicitly disposable",
"acceptance": "the full behavioural harness passes at every commit on the primary platforms",
"deadline_or_stop": "stop and keep the hybrid if defects concentrate in the FFI layer rather than the ported code",
"rollback": "git revert per component; the previous implementation still builds"
},
{
"step": 3,
"title": "Set the deletion date and hold it",
"owner": "the maintainers, collectively",
"cost_range": "1 release cycle; budget 2x the estimate",
"artifact": "a commit that deletes the last file of the old language, followed within one week by removal of the interop crates",
"acceptance": "the harness passes with the old language absent, and the interop dependencies are gone from the lockfile",
"deadline_or_stop": "keep the hybrid indefinitely if the deletion date slips twice",
"rollback": "available at every commit until the deletion lands; after that, none"
},
{
"step": 4,
"title": "Ship the feature you chose the language for",
"owner": "the maintainer who proposed the port",
"cost_range": "1-2 releases after the port",
"artifact": "the previously blocked capability, behind a feature flag, in a shipped release",
"acceptance": "the capability is enabled in a release within two cycles of the port completing",
"deadline_or_stop": "record the language-selection argument as untested if it has not shipped by then",
"rollback": "feature flag; the port stands on its other benefits regardless"
}
],
"assumptions": [
"The pre-port C++ baseline is measured at tag 3.6.0 (2023-01-07), three weeks before the proposal PR opened. Tags 3.7.0 and 3.7.1 are a maintenance branch carrying no Rust, so they are not the port's baseline; the 3.7.1 tree confirms this with 104 .cpp files and zero .rs files.",
"Distinct author counts come from the GitHub commits API `author.login`, falling back to the commit author name where no account is linked. One distinct value is counted as one person, so an author using two identities counts twice.",
"'unsafe occurrences' counts the word `unsafe` with word boundaries across all tracked .rs files including tests and build.rs. One occurrence sits in a comment. It is a count of occurrences, not of distinct unsafe blocks, and the mix at HEAD is 248 blocks, 17 `unsafe fn` and 16 `unsafe impl`.",
"The assessment treats doc_internal/fish-riir-plan.md as the RFC and its four stated goals as the requirement, because no separate requirements document exists.",
"Line counts for tags 3.6.0 and 4.0.0 come from source tarballs fetched through `gh api`; byte-level comparisons against Emacs and remacs come from the git trees API, where sizes are bytes rather than lines and are labelled as such."
],
"challenge_audit": {
"migration_case": [
{
"id": "M1",
"name": "Attribution",
"state": "PASS",
"claim": "The project never credited Rust with speed. Its own retrospective reports parity, and the 4.0.0 notes open by telling users to expect no difference. This report does not upgrade that into a performance win.",
"evidence": "CHANGELOG.rst:727 · https://fishshell.com/blog/rustport/"
},
{
"id": "M2",
"name": "Baseline and regime",
"state": "PASS",
"claim": "The C++ baseline is measured at tag 3.6.0, three weeks before the proposal, not at a convenient later point. The .cpp-only figure excluding fish_tests.cpp is 56,983 lines, which reconciles with the project's own published '57k'.",
"evidence": "3.6.0 tarball: 64,328 .cpp lines total, 56,983 excluding fish_tests.cpp"
},
{
"id": "M3",
"name": "Omitted cost",
"state": "HIT",
"claim": "The RFC priced neither the schedule overrun nor the release freeze. Six months became 338 days to delete the C++ and 761 to ship, with 345 days of no releases in between.",
"evidence": "D11 · doc_internal/fish-riir-plan.md:73 · gh api releases"
},
{
"id": "M4",
"name": "Compatibility and dual-run",
"state": "PASS",
"claim": "Both languages ran in one binary for eleven months, the acceptance harness was black-box and already existed, and the old implementation built at every commit until 102ab2c90.",
"evidence": "D10 · doc_internal/fish-riir-plan.md:28-34 · commit 102ab2c90"
},
{
"id": "M5",
"name": "Promise not kept",
"state": "HIT",
"claim": "Two of the RFC's own commitments are outstanding. Concurrent mode, the tiebreaker that chose Rust, has not shipped. CMake, which the RFC said would be replaced, is still the recommended build.",
"evidence": "D7, D8 · doc_internal/fish-riir-plan.md:36, 67-69 · issue 238"
}
],
"staying_case": [
{
"id": "S1",
"name": "Funded counterfactual",
"state": "PASS",
"claim": "Two in-stack options are retained and priced: modernize the C++ standard, and ship the threading branch that already worked. Neither is hypothetical.",
"evidence": "D12 · options stay-modernize and stay-concurrent-cpp"
},
{
"id": "S2",
"name": "Cost of inaction",
"state": "HIT",
"claim": "Staying meant 17 people in 11 years stays the number. For a volunteer project that is the constraint that eventually ends it, and the report does not treat 18 years of shipping C++ as evidence it would keep working.",
"evidence": "D1 · https://fishshell.com/blog/rustport/"
},
{
"id": "S3",
"name": "Status-quo bias",
"state": "HIT",
"claim": "'It compiles and ships' was true of the C++ throughout. It says nothing about whether the next maintainer will show up, which is what the RFC asked about.",
"evidence": "D1 · D9"
},
{
"id": "S4",
"name": "Native-advantage denial",
"state": "PASS",
"claim": "The report never claims Rust made fish slower or faster. It records the project's own parity statement and names the profile that would settle it.",
"evidence": "D3 · gap 1"
},
{
"id": "S5",
"name": "Stop condition",
"state": "PASS",
"claim": "The RFC set a deletion target and the project hit it in 338 days. Compare curl's hyper backend: four years, no date, abandoned at 95%.",
"evidence": "doc_internal/fish-riir-plan.md:28 · precedent: curl · the hyper backend"
}
]
}
}
</script>
</body>
</html>