shelly-shell 0.4.0

A Rust based Unix style shell with a typed and structured language syntax.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
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
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
# Shelly

A Unix-style shell written in Rust, growing toward a language for working with
commands, structured data, and network services in the same place.

<p align="center">
  <img src="./Shelly.png" alt="Shelly" width="50%">
</p>

Shelly is early work. Development of the shell is already being done in the shell:
running builds, using development tools, and trying new features from its prompt.
The examples below describe the current implementation.

## Build and run

Shelly runs on Linux (including WSL) and macOS. Build with a Rust toolchain
supporting edition 2024.

```sh
git clone https://github.com/cstrainge/shelly.git
cd shelly
cargo build --locked
cargo run --locked
```

Shelly supports interactive use, script files, command-line source, and stdin:

```sh
./target/debug/shelly                         # Interactive when attached to a terminal
./target/debug/shelly example.shy one two     # Execute a file
./target/debug/shelly -c 'echo $args...' one two
printf 'echo "hello"\n' | ./target/debug/shelly -s
```

Without a file or `-c`, noninteractive input is read from stdin. `-i` forces the
REPL. `$args` is an array of the supplied arguments, excluding the script filename;
`$args...` passes its elements as separate arguments. Shell options go before the
script filename or script arguments. Use `--help` for all options.

In the REPL, Ctrl+Enter switches to multiline entry without changing prompt width.
The prompt marker and continuation `>` turn yellow on 256-color and true-color
terminals while multiline entry is active. Monochrome mode disables editor and
default-prompt coloring.
Enter then inserts a newline at the cursor; Ctrl+Enter again submits the
whole buffer and restores normal entry. Ctrl+C discards the buffer and restores
normal entry. Outside multiline mode, Enter submits as usual. Shift+Enter always
inserts a newline. Definitions and variables persist between submissions.
Leave with `exit` or Ctrl+D. Tab completes a unique name or common prefix; a
second Tab opens the completion menu. Arrow keys navigate an open menu.
Ctrl+Enter and Shift+Enter require a terminal that reports those key combinations.

Interactive startup loads `~/.shelly_init.shy`. `--rcfile PATH` selects another init
file; `--norc` skips it. `-l` enables login startup, which loads
`/etc/shelly/profile.shy`, then `~/.shelly_profile.shy`, before interactive init.
The standard prelude loads once after those profiles and before the init file,
unless a profile has already loaded it with `prelude_reload`. See
[Modules](#modules) for standard-library paths, prelude selection, and profile examples.
`--norc` does not disable login profiles. Script, `-c`, and stdin modes skip
interactive init. `-b` suppresses the banner; `-m` requests monochrome output.

Define `fn prompt() { ... }` in the init file to customize the prompt. Shelly uses
its printed stdout as the prompt text and falls back to the default prompt if
the call fails.

Before each prompt, Shelly calls the functions in `$widgets` in order. Each returned
nonempty string is enclosed in brackets, and the results are joined without a separator.
An empty string contributes no text or brackets; whitespace is preserved.
The temporary `String` variable `$rendered_widgets` holds that text only while
`prompt` runs; an empty widget array produces `""`. Widget stdout is discarded.
Widget or prompt errors use the default prompt, and later cycles can recover.

`$last_cmd_time` is a `String` containing the elapsed time of the last submitted
REPL command, including failed commands. It starts as `"0:00"` and uses whole
seconds: `mins:seconds`, `hours:mins:seconds` from one hour, and
`days:hours:mins:seconds` from one day (for example `"2:05"`, `"1:02:05"`, and
`"1:03:02:05"`). Typing, widgets, and prompt evaluation are excluded. Every new
prompt gets fresh timing: empty or whitespace-only input and editor cancellation
reset it to `"0:00"`, even when the prompt function prints nothing.
Prompts and widgets can both read it:

```text
$widgets = [$widgets..., fn (): String { "took ${last_cmd_time}" }]
```

The standard library's [std/widgets.shy](std/widgets.shy) provides these widgets.
Import the ones you want and register their function references in `$widgets`:

| Widget | Example rendered text | Behavior |
| --- | --- | --- |
| `clock_widget` | `[14:32]` | Local time in 24-hour format (`HH:MM`). |
| `clock_12_widget` | `[02:32 PM]` | Local time in 12-hour format with AM/PM. |
| `git_widget` | `[git: main]` or `[git: main, 2 changed]` | Git branch, with a count of status entries when there are changes. Empty outside a Git repository or if either Git command fails. |
| `command_time_widget` | `[Took: 0:02]` | Last command's duration. Empty when `$last_cmd_time` is `"0:00"`, including commands shorter than one second. |
| `venv_widget` | `[venv: myenv]` or `[venv: inactive]` | Active Python environment's name. Empty outside a Python workspace. |
| `node_widget` | `[node: 20.11.1]` or `[node: expected 20.11.1, current: 22.0.0]` | Current Node version, with the expected version when it differs. Empty outside a Node project. |

Both clock widgets return `""` if `date` fails. Empty results contribute no brackets.
Use `clock_12_widget` instead of `clock_widget` for a 12-hour clock, and add
`command_time_widget` to show command durations alongside the clock and Git status.

The workspace widgets search the current directory and its parents without changing
the working directory. Python markers are `pyproject.toml`, `setup.py`, `setup.cfg`,
`requirements.txt`, and `.venv` or `venv` directories. Within a Python workspace,
`venv_widget` shows the basename of `$VIRTUAL_ENV`, or `venv: inactive` if it is unset
or empty.

Node markers are `package.json`, `.nvmrc`, and `.node-version`. At the nearest project
root, `node_widget` reads the expected version from `.nvmrc`, then `.node-version`,
then `package.json`'s `engines.node`. It compares that text with `node --version`,
ignoring a leading `v`. Exact matches show only the current version; ranges and
aliases remain visible as the expected version. Missing Node displays `node: missing`,
with the expected version when a version file provides one. Widget child processes
use the shell's current exported environment, so changing `$VIRTUAL_ENV` or `$PATH`
after importing the widgets takes effect at the next prompt.

To add both workspace widgets alongside those already registered:

```text
import std::widgets::{ venv_widget, node_widget }
$widgets = [$widgets..., `venv_widget, `node_widget]
```

For example, add this to `~/.shelly_init.shy` to register the standard-library
clock and Git widgets and display them on the first prompt line:

```text
import std::widgets::{ clock_widget, git_widget }

$widgets = [$widgets..., `clock_widget, `git_widget]

fn prompt()
{
    echo "\n<shelly> ${rendered_widgets}"
    echo ": ${pwd}"
}
```

The backticks store function references for later execution, and `$widgets...`
preserves any widgets already registered. Their order in `$widgets` controls their
display order. A prompt might look like this:

```text
<shelly> [14:32][git: main, 2 changed]
: ~/workdir/shelly
$
```

Place `${rendered_widgets}` anywhere in the text printed by `prompt` to position
the widget group. For example, this puts it after the working directory instead:

```text
fn prompt()
{
    echo "\n<shelly>"
    echo ": ${pwd} ${rendered_widgets}"
}
```

You can also use `echo $rendered_widgets` to give widgets their own line. The Git
widget returns `""` outside a Git repository, so it contributes no brackets there.

## Statements, values, and arithmetic

Commands use space-separated arguments. Newlines and semicolons separate
statements; `#` starts a comment. A backslash followed by a newline continues a
logical source line, including inside arithmetic and return expressions. Put spaces
around binary arithmetic operators; braces may touch the expressions they enclose.

Declare variables with `let`, reference them with `$name` or `${name}`, and assign
an existing variable with `$name = expression`:

```text
let $x = 1024 + 2 * 2
echo $x                     # 1028
$x = ($x - 4) / 2
echo $x                     # 512
```

Variable names cannot contain `=`, including braced names and function parameters.
Keep spaces around assignment `=`; adjacent `==` and `!=` remain comparisons.

Use `let _ = expression` to evaluate an expression once and discard its value
without creating a variable. This also consumes a command's exit status, just as
assigning it to a variable does; evaluation errors still propagate. Printed output
is unaffected. The discard requires an initializer and has no type annotation or
`export` modifier. `let $_ = expression` remains an ordinary named binding.

An optional `: Type` annotation constrains a variable's initializer and subsequent
writes. Type names are case-sensitive: use `Number`, not `number`.

```text
let $count: Number                 # Defaults to 0
let $label: String = "items"
let $values: [Number] = [1, 2.5]
let $lookup: [String: Number]
let $maybe: optional Number        # Defaults to ()
$count = 3.5
$lookup["count"] = $count
```

Annotated declarations may omit `= expression`. Defaults are:

| Type | Default |
| --- | --- |
| `Number`, `Integer`, `Float` | Zero of the appropriate numeric type |
| `Boolean` | `false` |
| `String` | Empty string |
| `[T]`, `Array` | `[]` |
| `[K: V]`, `HashMap` | `[:]` |
| `optional T`, `None`, `any` | `()` |
| `Range` | `0..0` |
| `ExecResult` | `ExecResult(0)` |
| `ArgumentExpansion` | Empty argument expansion |

Structs and enums require an initializer unless wrapped in `optional`; empty
containers of those types need no initializer. `let $x` without a type or an
initializer remains an error. Annotations use the same types and nested container
constraints as struct fields. `Number` accepts integers and floats, but does not
coerce strings or booleans. Failed initializers and writes preserve the previous
binding, including its constraint. Indexed and field writes validate the updated
value before committing it.

Unannotated bindings remain dynamically typed. A new `let` declaration may replace
an existing binding and its annotation; inner declarations shadow outer bindings.
An `Integer` is promoted to `Float` wherever a Float is required: declarations,
assignments, parameters (including variadic parameters), return values, struct
fields, and typed collection elements or values. Promotion applies recursively
through optional types and nested collections. Strings and booleans are not
implicitly promoted. `Number` preserves whether its value is an Integer or Float.
Other typed bindings preserve the assigned value's type, including
`ArgumentExpansion`; untyped assignments retain their expansion-to-array conversion.
A Float never implicitly narrows to Integer; use `Integer(...)` explicitly.
That checked conversion rejects fractional and out-of-range values.
Promoted, converted, and computed Floats display with a decimal point (`7.0`)
or scientific notation for very large or small values. Float literals retain
their original spelling.

```text
let $x: Float = 7
echo $x                   # 7.0
echo ($x / 2)              # 3.5
$x = 9
echo ($x / 2)              # 4.5
let $n: Integer = Integer(8.0)
echo ($n / 2)              # 4
```

Values include signed 64-bit integers, floating-point values, booleans, strings,
arrays, hash maps, ranges, enums, structs, the no-value result displayed as `()`, and external
command statuses such as `ExecResult(0)`. Arrays come from literals, `$args`, and
file globs. Without `...`, an array becomes colon-separated text when passed to
a command. `()` is also a literal that evaluates to `None`, including in assignments
and returns.

Use `$args...` or `${args}...` to expand command arguments. A splat cannot be
the executable: `$cmd... 2` is a parse error; use `$cmd 2` to call a stored command.

Arithmetic supports `+`, `-`, `*`, `/`, and `%`, with normal precedence,
left associativity, and parentheses. Integer operands produce Integer results:
`7 / 2` produces `3`, truncating toward zero. If either operand is a Float, the
other numeric operand is promoted and the result remains a Float: `7.0 / 2`,
`7 / 2.0`, and `7.0 / 2.0` all produce `3.5`. This applies to all five operators;
`2.5 + 1` produces `3.5`, and `7.5 % 2` produces `1.5`.

When both operands are Strings, `+` concatenates them: `"Hello, " + "world!"`
produces `"Hello, world!"`, and `"7" + "2"` produces `"72"`. The result is plain
string data, even when an operand carries an executable marker.

All other arithmetic requires numeric operands. Strings (including numeric text),
booleans, arrays, maps, ranges, structs, enums, command statuses, and `()` produce
errors. Use an explicit numeric conversion when needed, such as `Integer("7") + 2`.
Signed numbers and unary minus work: `-2 + 3` produces `1`, and `-(2.5 + 3)`
produces `-5.5`. Division/remainder by zero, integer overflow, and non-finite
floating-point operands or results produce language errors, including in release builds.

Expressions can stand alone, including inside functions. A top-level expression
is evaluated without automatically printing its value; use `echo` to display it.

## Arrays

Create an empty array with `[]` or use comma-separated expressions:

```text
let $a = [10, 20 + 2, "three", [4, 5]]
echo $a[1]                   # 22
$a[1] = 99
$a[3][0] = 40
echo $a[3]...                # 40 5
echo [7, 8][0]               # 7
```

Elements evaluate left to right and retain their types. Nested arrays remain
nested. Newlines, comments, and a trailing comma are allowed between elements.
Each element accepts the same value expressions as an initializer, including
calls and conditionals: `[foo 3, if true { 1 } else { 2 }]`.

Indexes are zero-based integers. `$a[$i + 1]`, `${a}[0]`, `(make_array)[0]`,
and chained `$a[1][2]` reads work. The opening index bracket must touch its
value: `$a[0]` is an element, while `$a [0]` supplies a separate array argument.
Negative, out-of-range, or non-integer indexes (including `"0"` and `1.0`)
produce errors for arrays. Only arrays and maps support indexing. Array writes
replace existing elements; they do not append or grow an array. Numeric function
arguments retain their types; use `Integer($index)` to convert an explicitly textual
index, such as one read from `$args`.

Indexed assignment must start with a variable, as in `$a[0] = value` or
`$a[0][1] = value`. It updates the nearest visible binding. Index expressions
evaluate once, left to right, followed by the right-hand expression; the complete
path is checked before replacing the element. Evaluation side effects remain if
a later check fails. Assigning or reading an array copies its value: changing
`$b` after `let $b = $a` does not change `$a`.
Internally, arrays and argument expansions share reference-counted storage;
indexed writes copy shared arrays only along the modified path.

Use `...` to spread an array into arguments or another literal:

```text
let $a = [2, 3]
let $b = [1, $a..., 4]        # [1, 2, 3, 4]
echo $b...                    # 1 2 3 4
```

Indexed executable strings follow the same call rules as executable variables:
`$commands[0] 3` calls with an argument; `$commands[0]` invokes a marked executable
in statement position. As a command argument it passes the reference unchanged;
use `echo ($commands[0])` to call it and pass the result. Indexing is expression
syntax; quoted string interpolation still supports variable names rather than
arbitrary expressions.

An array cannot be a command. A standalone `$a` or `$a[0]` whose value is an array
and is discarded reports `Cannot execute an array as a command`; explicit calls
such as `$a "argument"` reject arrays too. Arrays remain valid as function return
values, conditional results, and arguments (`echo $a` or `echo $a...`).

A leading `[` starts an array or map literal. For bracket globs use a path prefix,
such as `./[ab].txt` or `fixtures/[ab].txt`; quote brackets to pass literal text.

## Builtin type methods

Methods use member access and ordinary shell-style arguments. Arrays and argument
expansions (including glob results) provide these methods:

| Method | Result |
| --- | --- |
| `$items.sort` | A new array sorted in ascending order; the receiver is unchanged |
| `$items.zip $other` | An array of two-element arrays, stopping at the shorter input |
| `$items.count` | The number of elements as an `Integer` |

```text
let $items = [3, 1, 2]
let $sorted = $items.sort
echo $sorted...                         # 1 2 3
echo $items.count                       # 3
let $pairs = $sorted.zip ["a", "b"]
echo $pairs[0][0] $pairs[0][1]           # 1 a
echo ($items.zip [4, 5]).count           # 2
let $files = (./src/*.rs).sort
```

Methods with no arguments run when accessed, including in assignments and chained
expressions such as `$items.sort.count`. Calls with arguments use spaces;
parenthesize a nested call as in `echo ($items.zip $other)`. Empty parentheses
are still a `None` argument, so use `.sort`, not `.sort()`.

Sorting is stable: equal values retain their relative order. Numbers sort
numerically, including mixed integers and floats without rounding large integers
before comparison. Strings sort lexicographically by their text, booleans put
`false` first, and variants of one enum sort by `.index`. Mixed categories,
different enum types, nonfinite numbers, and structured elements are rejected.
Empty arrays are valid. Sorting strings does not execute them.

Zip preserves element types and value semantics, including nested collections.
Its argument must be an array; explicitly expanded arguments still follow normal
command expansion rules. Use `Array($expansion)` to pass an argument expansion as
one array. For example, `[1, 2].zip ["a", "b"]` produces
`[[1, "a"], [2, "b"]]`. Iterate over those arrays with a destructuring pattern:

```text
for ($number, $letter) in $items.zip ["a", "b"]
{
    echo $number $letter
}
```

Use `for $pair in ...` to keep each two-element array as a single value.

Backtick access retains a callable method bound to its live receiver:

```text
let $sort = `$items.sort
$items[0] = 9
echo ($sort)...                         # 1 2 9, using the updated receiver
```

Methods are registered per type in the type registry. Adding a builtin method
does not require a new parser rule or a separate opcode for each method. Struct
fields and enum `.index` retain their existing behavior.

## User-defined type methods

Use `fn TypeName::method(...)` to extend any visible type, including builtin types,
structs, and enums. The method does not have to be declared alongside the type.
An implicit, typed `$self` parameter receives the value before the explicit
parameters. Do not declare `$self` in the parameter list.

```text
struct Item { quantity: Integer }

fn Item::add($amount: Integer): Item
{
    $self.quantity = $self.quantity + $amount
    $self
}

let $item = Item(quantity: 2)
let $updated = $item.add 3
echo $item.quantity $updated.quantity   # 5 5

fn Array::first($fallback: optional any): any
{
    if $self.count == 0 { $fallback } else { $self[0] }
}

echo [4, 5].first                       # 4
echo ([].first "empty")                 # empty

fn Integer::sum($rest: Integer...): Integer
{
    for $value in $rest { $self = $self + $value }
    $self
}

let $start = 10
echo ($start.sum 1 2 3)                 # 16
```

Methods use the same parameter annotations, trailing optional parameters, final
variadic parameter, return annotations, explicit `return`, and implicit final
result as ordinary functions. Calls use the same dot syntax as builtin methods.
`$self` is a mutable alias to the receiver. Assigning `$self` or updating its
members changes the caller's variable immediately; returning or reassigning the
result is not required. This also works through nested fields and array or map
indexes, such as `$items[0].add 3`. Receiver indexes are evaluated once.

Ordinary value copies and explicit function parameters remain independent.
Methods on literals, constructor results, or other temporary values mutate only
that temporary. Returned values are ordinary values, so chaining a method onto
a returned value operates on that result. Existing type constraints still apply
to mutations, including constraints on containing fields and collections.
Successful mutations remain visible if a later statement in the method fails.

Extensions on `Number` apply to both integers and floats; extensions on `any`
provide a fallback for all values. Methods on the concrete type take precedence,
followed by `Number` for numeric receivers, then `any`. A user method may replace
a builtin method on that type. Struct fields and enum `.index` take precedence
over fallback methods, and declaring a method directly on a type with the same
name as one of its data members is an error.

Methods follow ordinary function scoping and forward-declaration rules. Different
types may use the same method name, and method names do not occupy the namespace
of bare function calls. Already compiled calls retain their visible method
versions when a method is redefined. Backtick references such as
``let $add = `$item.add`` keep a live reference to the receiver binding and pin
the method version; `$add 3` updates `$item`. Reassigning the same binding is
visible through the reference, while declaring a new variable with `let` creates
a separate binding. Captured bindings remain alive after their scope exits.
References to collection elements retain their evaluated index or key; an invalid
path or incompatible receiver type produces an error when called. Redefining a
type creates a distinct type, so old values retain their original methods.

## Hash maps

Use `[key: value, key: value]` to create a map and `[:]` for an empty map.
`[]` remains an empty array. Keys and values are expressions, evaluated left to
right, key before value. Newlines and a trailing comma are allowed; array elements
and map pairs cannot be mixed in one literal. Quote literal string keys to avoid
the usual bare-word command lookup rules.

```text
let $myHash = ["key": 42, "items": [1, 2]]
let $x = $myHash["key"]          # 42
echo $myHash["missing"]          # ()
$myHash["new"] = 7              # Insert
$myHash["key"] = 99             # Replace
$myHash["items"][0] = 8         # Nested write
```

Any value can be a key, including arrays and maps:

```text
let $lookup = [[1, 2]: "array key", ["a": 1]: "map key"]
echo $lookup[[1.0, 2]]           # array key
echo $lookup[["a": 1.0]]         # map key
```

Keys compare by value. `1` and `1.0` identify the same key, while `"1"` is distinct.
String execution flags and float source spelling do not affect key identity;
array order matters and map entry order does not. Collection keys are immutable
snapshots: modifying the original array or map leaves its stored key unchanged.
NaN keys are canonicalized to one key so they can be looked up reliably. Duplicate
keys keep the last value, while all key and value expressions still execute.

Maps use reference-counted storage and copy-on-write, like arrays. Indexed writes
insert or replace the final key; missing intermediate containers cause an error.
A missing read returns `()` without inserting an entry. Maps compare by their
entries and convert to false only when empty. Arithmetic on maps is an error.
Text conversion produces a bracketed list of key/value pairs in a
stable order, with quoted strings and bracketed nested collections. Passing a map
as an argument uses that text; `...` treats a map as one value and does not iterate
its entries. A map cannot be executed as a command.

Inside a collection literal, `:` separates map keys and values. Use parentheses
for a nested call that takes an unquoted colon argument, such as
`["result": (command :)]`.

## Ranges

Ranges hold integer bounds without allocating their elements:

| Syntax | Meaning |
| --- | --- |
| `1..5` | Start included, end excluded |
| `1..=5` | Both ends included |
| `1..` | Start specified, end omitted |
| `..5` or `..=5` | Start omitted, end excluded or included |
| `..` | Both bounds omitted |

Either bound can be a variable or an expression. Bounds evaluate once, left to
right, when the range is created; later variable assignments do not change it.

```text
let $start = 1
let $end = 4
let $r = $start..$end
echo $r                       # 1..4
echo $r...                    # 1 2 3
echo ($start..=$end)...        # 1 2 3 4
let $inner = ($start + 1)..($end - 1)
let $items = [0, $r..., 4]     # [0, 1, 2, 3, 4]
```

`...` expands a bounded range in ascending steps of one, materializing its
elements as arguments or array elements. Reversed ranges expand to no values;
`3..3` is empty and `3..=3` contains one value. Expansion is reusable and does
not consume the range. Parenthesize a literal before expanding it: `(1..4)...`.
Without expansion, a command receives the range's text, such as `1..4`.

Bounds must be integers; floats, numeric strings, and other types produce an
error. Use `Integer($start)` to explicitly convert a numeric string; integer function
arguments already retain their type. Inclusive ranges require an end bound, and chained
ranges such as `1..2..3` are rejected. Omitted bounds remain unspecified;
expanding such a range is an error. Range indexing and array slicing are not
implemented yet.

Ranges compare by their bounds and inclusivity: `1..3` differs from `1..=2`
even though they expand to the same elements. They can be map keys and function
return values, but cannot be commands. Boolean conversion is false for an empty
bounded range and true otherwise. Explicit numeric conversion of a range and
arithmetic on a range are errors.

Arithmetic binds more tightly than range operators, which bind more tightly than
comparisons. Spaces around `..` and `..=` are optional. Parenthesize open ranges
when followed by other arguments, as in `echo (1..) (..5)`. Ordinary words retain
embedded dots (`file..name`); quote text that would otherwise parse as a range.
Paths such as `./file`, `../file`, and `cd ..` continue to work.

## Enums

Enums define named alternatives. The current implementation supports unit variants:

```text
enum Color
{
    Red,
    Green,
    Blue,
}

let $color = Color::Green
echo $color                       # Color::Green
echo ($color == Color::Green)      # true
let $labels = [Color::Red: "stop", Color::Green: "go"]
echo $labels[$color]               # go
let $index: Integer = $color.index # 1
echo Color::Blue.index             # 2

fn is_green($value) { $value == Color::Green }
echo (is_green $color)             # true
```

Commas separate variants; trailing commas and newlines are allowed. Names contain
letters, digits, or underscores and cannot start with a digit or use a reserved
keyword. Empty enums, duplicate variants, and duplicate type declarations in the
same scope and submission are errors. Unit variants have no constructor arguments:
use `Color::Red`, not `Color::Red()`. Payload variants are not implemented yet.
Unit variants can be used as values in `match` arms.

Enum names are lexical: a declaration is available throughout its containing block,
including earlier expressions and function definitions. Inner declarations can
shadow outer types. The name does not escape its block, but returned or assigned
values retain their definition. Repeated calls and loop iterations reuse the same
compiled declaration's identity.

Enums compare by declaration identity and variant. A variant differs from its
printed string and from identically named variants of other declarations. Enums
can be array elements, map keys, function arguments, and return values. They always
convert to true, even if a variant is named `False` or `Error`. Arithmetic,
indexing, iteration, and using an enum as a command are errors. Expansion with
`...` passes one value; enum text such as `Color::Green` is display output, not
serialized source.

Every enum value has a read-only `.index` member: an `Integer` starting at zero
in declaration order. It works on variables, literal variants, and enum values
inside collections or struct fields. The index belongs to the value's original
declaration, so redeclaring an enum does not change existing values' indexes.
Use `.index` for numeric access; `Integer($color)` does not convert an enum directly.

The shared type registry persists across REPL submissions. Redeclaring a type in
a later submission creates a new identity; existing values and previously compiled
functions retain the old definition. Checking resolves enum names and variants in
the entire submitted AST, including unused functions and skipped branches, before
bytecode generation. A parsing, checking, or compilation failure does not publish
new types or functions. A runtime failure occurs after declarations are committed.

## Structs

Structs declare bare field names without `$`. A type annotation follows a colon:
`name: Type`. Omitting the annotation makes the field accept any value:

```text
enum Status { Ready, Busy }

struct Item
{
    quantity: Number,
    state: Status,
    children: [Item],
    labels: [String: String],
    next: optional Item,
    payload: any,
}

let $item = Item(
    quantity: 2,
    state: Status::Ready,
    children: [],
    labels: ["name": "widget"],
    payload: (),
)
echo $item.quantity $item.labels["name"]    # 2 widget
echo $item.next                            # ()
$item.quantity = 3.5
$item.labels["name"] = "updated"
```

Commas separate declarations and constructor arguments; newlines, comments, and
trailing commas are allowed. Declaration names and constructor labels omit `$`.
The opening parenthesis can touch the type name, be separated by spaces (`Item (quantity: 42, ...)`),
or follow it on a new line, including across blank lines and comments:

```text
let $item = Item
    (
        quantity: 42,
        state: Status::Ready,
        children: [],
        labels: [:],
        payload: (),
    )
```

The newline form requires named fields. Constructors with no supplied fields use
`MyType()` or `MyType ()`, with the opening parenthesis on the same line as the
type name. The type registry distinguishes `MyType ()` from a function call:
declared type names take precedence; otherwise `foo ()` passes `None` to `foo`.
A following ordinary grouped expression, including `()`, remains a separate
statement across a newline. Semicolons always end the statement. Shell-function
calls retain space-separated arguments, including `foo (expression)` on one line.

| Declaration | Meaning |
| --- | --- |
| `field` or `field: any` | Required field accepting any value, including `()` |
| `field: optional` or `field: optional any` | Any value; defaults to `()` when omitted |
| `field: Number` | Integer or float; strings and booleans do not qualify |
| `field: MyEnum` or `field: MyStruct` | A value of that specific type declaration |
| `field: [T]` | Array whose elements satisfy `T` |
| `field: [K: V]` | Map whose keys satisfy `K` and values satisfy `V` |
| `field: optional T` | Either `T` or `()`; defaults to `()` when omitted |

The existing builtin names also work, including `Integer`, `Float`, `String`,
`Boolean`, `None`, `Range`, `Array`, `HashMap`, and `ExecResult`. Annotations check
types and promote Integer values wherever Float is required, including inside
typed collections. Other conversions require explicit `Type(value)` syntax.
`Array` and `HashMap` accept those containers
without constraining their contents; `any` accepts every value.

Container annotations compose: `[String: [Item]]`, `[MyEnum: optional Item]`,
`[optional Number]`, and `optional [String: any]`. An array of optional elements
still requires an array; an optional array may itself be `()`. Empty arrays and
maps satisfy their respective element constraints. Map-key constraints follow
key equality: `1` and `1.0` denote the same key. This applies recursively to
collection keys; struct keys retain a typed snapshot of their original fields.

Every nonoptional field must be supplied, including untyped fields. Optional
fields may be omitted or supplied explicitly. Unknown and duplicate fields are
errors. Supplied expressions evaluate once, left to right in source order; fields
are stored and displayed in declaration order. Empty structs are allowed:
`struct Empty {}` and `Empty()`.

Read and update members with `$item.field`, including mixed paths such as
`$item.groups["name"][0].field`. An assignment must start with a variable.
Fields cannot be added or removed, and unknown members are errors. Accessing a
field through `()` is an error; an optional field containing a struct permits
normal member access. User-defined methods use `fn TypeName::method(...)` declarations;
optional-chaining syntax is not implemented.

Member syntax preserves word interpolation: `$item.field` accesses a field,
`${name}.txt` concatenates text, and `(${item}).field` accesses a field using a
braced variable. String interpolation still accepts variable names rather than
member expressions. Literal paths such as `file.txt` retain their meaning.

Structs have value semantics and share reference-counted storage. Assignment,
function calls, and collection insertion preserve their type; writes copy shared
values along the modified path. A method's implicit `$self` instead aliases its
receiver binding, so its mutations update that binding. All nested writes must satisfy the containing
field's constraints. An invalid update leaves the target unchanged, although
side effects from evaluating its indexes and right-hand expression remain.

Structs and enums share the same lexical type namespace, forward-reference rules,
and REPL identity rules. A scope cannot declare an enum and a struct with the
same name. An inner declaration may shadow an outer type. Type names `any` and
`optional` are reserved. Redeclaration in a later REPL submission creates a new
identity; existing values, field annotations, and compiled functions retain the
old definitions.

Self and mutual references are supported through optional fields or containers.
Cycles consisting entirely of required struct fields are rejected because they
cannot form a finite, fully initialized value. For example, `next: optional Item`
and `children: [Item]` are valid; a required `next: Item` inside `Item` is not.

Equality compares declaration identity and all field values. Structs can be map
keys, using immutable snapshots and normal value equality. They always convert
to true, including empty structs, and expansion with `...` passes one value.
Arithmetic, execution, iteration, and bracket-indexing a struct are errors; use
member access for its fields. Text such as `Item(quantity: 2, ...)` is display
output, not a serialization format.

The AST checker rejects invalid declarations, constructor shapes, known value
mismatches, and known invalid member accesses before executing the submitted
source. Dynamic values are checked during construction and updates. A failed
check or compilation does not publish new types or functions; runtime failures
occur after declarations have been committed.

## Distinct named types and unions

`type` declares a new identity backed by an existing type or type expression:

```shy
type Count = Integer
type MyHash = [String: Integer]
type Foo = Integer | String | ()

let $count: Count = 7         # Wraps the integer in Count
let $other = Count(2)         # Explicit construction
let $total = $count + $other  # Still Count
echo Integer($total)          # Explicitly unwraps: 9

let $scores: MyHash = ["alice": 10]
for ($name, $score) in $scores { echo $name $score }
let $item: Foo                # A Foo containing ()
$item = "ready"
```

These are distinct types, not interchangeable names. `Count(7) == 7` is false,
and assigning a Count to an Integer requires `Integer($count)`. Annotations can
wrap compatible underlying values in variables, arguments, returns, struct
fields, and collections. Integer-to-Float promotion still applies inside the
wrapper. `Count(value)` wraps compatible values and can explicitly convert from
another named wrapper; parsing text requires `Count(Integer("7"))`.
`any(value)` preserves the named identity.

Named types inherit their underlying fields, methods, indexing, iteration,
boolean conversion, and display behavior. Same-type arithmetic, including unary
negation (`- $count`), preserves the named type. Arithmetic between distinct named
types, or between a named and an unnamed value, requires explicit conversion.
Methods retain their declared return types; an inherited method returning Integer
returns an Integer. Define methods with `fn Count::method(...)` to override or
extend the inherited behavior. Inherited mutating methods update the original
receiver while enforcing its named type's constraints.

Named collections yield their underlying items during indexing and iteration,
with the declared item types available to the checker. Named hash keys retain
their identity: use `Key("x")` to look up a key of type Key. Key annotations can
wrap keys on assignment; conversions that would merge distinct entries are
errors. Named structs are constructed by wrapping the underlying constructor,
for example `Position(Point(x: 1))`; named enums also support `State::Variant`.

A named type uses its underlying default when one exists. Structs and nonoptional
unions require an initializer. A named value containing `()` remains a distinct
value, so iterator termination still requires an actual `()` return. Declare
iterator results as `Item | ()`.

Named types follow the lexical scope, forward-reference, import/export, and REPL
identity rules of structs and enums. Redeclaration creates a new identity without
changing existing values or compiled annotations. Recursive `type` definitions
are rejected; recursion through optional or collection fields of structs remains
supported.

Union annotations accept any listed member:

```shy
let $value: Integer | String = 7
$value = "seven"
let $number: Integer | Float = 7  # Stays Integer
let $ratio: Float | Boolean = 7   # Promoted to Float
let $maybe: Integer | String | () # Defaults to ()
```

An existing member match preserves the value. Otherwise, implicit conversions
must produce one unambiguous result. For example, if A and B are distinct Integer
types, assigning `7` to `A | B` is ambiguous; use `A(7)` or `B(7)`. Unions without
`()` require an initializer. Parentheses group type expressions, and unions work
inside collection annotations, such as `[String: Integer | Boolean]`.
`T | ()` and `optional T` are equivalent.

## Function prototypes

Function prototypes are types, and can appear in `type` declarations or directly
in annotations:

```shy
type Transform = fn(Integer): String
type Factory = fn(): Transform

fn describe($value: Integer): String { String($value) }
let $convert: Transform = `describe
echo ($convert 7)                       # 7

let $callbacks: [fn(Integer): String] = [`describe]
echo ($callbacks[0] 8)                  # 8
```

The syntax is `fn(Type, Type, ...): ReturnType`, with a comma-separated list of
parameter types. `fn()` takes no arguments. Omitting `: ReturnType` means `()`:
`fn(Integer)` and `fn(Integer): ()` describe the same contract. This default is
specific to prototypes; existing function definitions without a return annotation
keep their dynamic return behavior. Prototypes require an initializer unless
made optional. Each prototype has a fixed argument count; a variadic function can
satisfy any prototype whose arguments it accepts.

`any` is a proper type name and explicitly permits values of any type:

```shy
fn first($args...): any { $args[0] }

let $number: fn(Integer): Integer = `first
let $text: fn(String): String = `first

echo ($number 42) ($text "hello")
```

Known incompatible signatures and argument counts are rejected when the reference
is bound. `any` parameters and returns, including unannotated function parameters,
allow more specific prototypes; every call checks its arguments and returned value.
For example, a function declared to return `any` can satisfy `fn(): Integer`, but
returning a String through that prototype is an error. Integer-to-Float promotion
and named-type wrapping apply at these call boundaries. Existing contracts remain
in force when a callable is assigned to another prototype, even one using `any`.

Prototypes accept bound scripted functions, native functions, and bound methods.
Native checks use available signature metadata and enforce the prototype at call
time. A plain string or external-command reference does not provide a function
signature. Native return values are preserved: `cd`, for example, can be used as
`fn(String): ExecResult`.

A typed reference preserves its function version or its method's live receiver.
Copying it does not call it: `let $copy = $convert` copies the reference;
`($convert 7)` calls it. Use `($factory)` to call a zero-argument factory and obtain
its returned function. Typed functions compare by callable identity and prototype,
can be hash keys, and are distinct from strings with the same display name.

Named prototypes remain distinct, with the same wrapping and explicit conversion
rules as other named types. They can be imported and used in fields, collections,
parameters, returns, and other prototypes. For a union of a function and `()`,
write `(fn(Integer): String) | ()`; without those parentheses,
`fn(Integer): String | ()` describes a function returning either String or `()`.
Recursive `type` definitions remain unsupported.

## Explicit type conversions

Use `Type(value)` to request conversion. All builtin types support this syntax.
Annotations implicitly promote Integer to Float wherever Float is required and
wrap compatible values in declared named types. Other conversions, including
Float to Integer and unwrapping named values, must be explicit. `Integer` and
`Float` are concrete numeric types; `Number` accepts either (`Integer | Float`).

| Target | Accepted input and behavior |
| --- | --- |
| `Integer` | Integers, integral floats, decimal integer text, booleans, numeric exit statuses |
| `Float` | Numbers, numeric text, booleans, and numeric exit statuses |
| `Number` | Preserves numbers; converts numeric text, booleans, and numeric exit statuses |
| `Boolean` | Uses the truth rules below |
| `String` | Display text for any value, with executable flags removed |
| `Array` | Arrays, argument expansions, and bounded ranges |
| `ArgumentExpansion` | Arrays, argument expansions, and bounded ranges |
| `HashMap` | Existing hash maps |
| `Range` | Existing ranges, including unbounded ranges |
| `ExecResult` | Existing statuses, integer-valued numbers, decimal integer text, or booleans |
| `None` | Evaluates its input, then produces `()` |
| `any` | Preserves the input value and its concrete type |

Numeric conversions map booleans to 0 or 1 and numeric command statuses to their
exit code. `ExecResult` requires a code in 0..=255; it maps `true` to status 0 and
`false` to status 1. `Number` parses text as an integer when it fits, otherwise as
a finite float; booleans and numeric command statuses become integers.

Numeric text may have surrounding whitespace. `Integer("2.0")` is rejected;
`Integer(Float("2.0"))` succeeds. Fractional float-to-integer conversions, overflow,
invalid text, and nonfinite numeric conversions are errors. Floating-point
conversion has normal `f64` precision limits. A status caused by a signal has no
numeric code, so it cannot convert to an integer or float.

Collection conversions preserve elements without converting them. Arrays and
argument expansions share their reference-counted storage; writes retain value
semantics. Bounded ranges materialize their integer elements. Other collection
conversions, including arrays of pairs to maps, are errors.

```text
let $count: Integer = Integer("42")
let $ratio: Float = Float("2.5")
let $items: Array = Array(1..4)
echo ArgumentExpansion($items)      # 1 2 3
let $status: ExecResult = ExecResult(false)
echo Boolean($status) Integer($status)  # false 1
```

Conversions require one expression. Newlines inside the parentheses and a
trailing comma are allowed. Builtin conversions also allow a space before `(`;
named type construction requires an attached `(`, as in `Count(7)`; a newline before `(` starts a
separate statement. Use `None(())`, for example, rather than an empty `None()`.
Type names take precedence over function names in this syntax. A struct or enum
declaration with a builtin name shadows that conversion. Resolved conversion
targets remain fixed in previously compiled functions. User-defined conversions
are not implemented yet.

## Boolean expressions

`==` and `!=` compare values and produce booleans. Numbers compare numerically,
including integer/float pairs; strings compare their text, ignoring executable
flags. Float source spelling does not affect equality. Arrays compare their
elements in order. Unrelated types are unequal: `"1" == 1` and `true == 1` are
false. `()` equals `()`. Command statuses compare as statuses, not as integers
or booleans.

`Boolean(value)` explicitly converts a value to a boolean. `!`, `&&`, and `||`
use the same conversion rules:

| Value | Boolean conversion |
| --- | --- |
| `()` | False |
| Boolean | Its existing value |
| Integer or float | False for zero, true otherwise |
| String | False for empty text (`""`); true for every nonempty string |
| Array, hash map, or argument expansion | False when empty, true otherwise |
| Range | False for an empty bounded range; true otherwise |
| Enum or struct | Always true, including empty structs |
| External command result | True for exit status 0; false for nonzero status or termination by signal |

```text
let $result = /bin/true
let $succeeded: Boolean = Boolean($result)  # true
echo Boolean(0) Boolean("false") Boolean([1])  # false true true
```

Boolean annotations check values without converting them: `let $flag: Boolean = 1`
is an error. `!!value` remains a shorthand for boolean conversion.
`Boolean(value)` requires one expression; use `Boolean(())` to convert `None`.

Logical operators always return a boolean. `&&` skips its right operand when
the left is false; `||` skips it when the left is true. Skipped operands have no
side effects and cannot cause runtime errors, but must still be valid syntax.

Newlines, blank lines, and comments may appear before or after `&&`, `||`, `==`,
and `!=`. A newline before an operator continues the expression; otherwise it
still ends the statement. Precedence and short-circuit behavior are unchanged:

```text
if    ($expected != "")
   && ($actual != $expected)
{
    echo "Output differs"
}
```

Precedence, highest first: parentheses and indexing; unary `!` and unary minus;
`* / %`; `+ -`; `.. ..=`; `== !=`; `&&`; `||`. Range operators cannot be chained;
other binary operators at the same precedence associate left to right. Boolean
operators do not require surrounding spaces, so `$x!=0` and `!$x` work. Quote
operator text when passing it literally.

```text
echo (1 == 2) (2.5 == 2.50)    # false true
echo !"false" !"0"             # true true
let $ready = 2 + 3 == 5 && !false
echo $ready                    # true
echo (false && (1 / 0))         # false; division is skipped
echo ((/usr/bin/false) || (/usr/bin/true))  # true
```

Use parenthesized calls to make command results operands: `(foo 3) && (bar 4)`.
These are value expressions, not shell command chains: `echo true && false`
passes the single value `false` to `echo`. Bare words within boolean expressions
are string operands; `foo == foo` compares text. Function parameters preserve
their argument types: passing `3` gives an integer, while passing `"3"` gives
a string. Equality does not coerce one into the other.

## Strings and paths

Double-quoted strings interpolate `$name` and `${name}`. Single-quoted strings
keep variable references literal. Missing variables are errors. Both quote forms
process backslash escapes, including `\n`, `\r`, `\t`, hexadecimal `\x41`, octal
`\o101`, and decimal `\065` (the last three produce `A`). Use `\$` inside double
quotes to keep a dollar sign literal: `"\$name"` produces `$name`. This also works
in multiline strings.

```text
let $name = 'Shelly'
echo "Hello, $name!"
echo "Building ${name}..."
echo '$name stays literal here'
```

Multiline strings use `"* ... *"` or `'* ... *'`. Leading whitespace before the
first text is skipped, and the first line establishes the indentation removed
from subsequent lines. Extra indentation and embedded newlines are preserved,
including the newline before a closing delimiter on its own line.

```text
let $project = 'Shelly'
echo "*
    Building $project
      Source: src/
      Mode: development
    *"
```

The double-quoted form interpolates variables; the single-quoted form keeps them
literal. Ordinary single-line quotes cannot contain a raw newline.

Reading variables and interpolating strings shortens paths under the current
`$HOME` to `~` or `~/...`, including `$pwd` and strings stored in arrays and map
values. Map keys retain their original values.
Only complete home-directory prefixes match; similarly named sibling directories
stay unchanged. Stored values are not rewritten by reading them.

Shelly expands leading `~` or `~/` at filesystem boundaries: `cd`, executable
lookup, glob variable prefixes, path settings, and external-command arguments.
This also applies to quoted or variable-derived arguments. Thus `cd $p` and
`cat "$p/file"` work with shortened paths, and `echo $pwd` prints an absolute
path. Embedded text such as `echo "cwd: ${pwd}"` retains the shortened path,
as does a custom prompt using `${pwd}` after its label or color codes. `~someone`
is not expanded. This external-command expansion does not apply at a shell
function call boundary; reading the function parameters follows the same path
shortening rules as other variable reads.

Unquoted paths can begin with a variable. Its value and the suffix remain one
argument, including spaces in the value:

```text
let $root = '/tmp'
echo $root/project/file.txt
let $name = 'report'
echo ${name}suffix           # reportsuffix
echo ${name}.txt             # report.txt
```

Braces mark the end of a variable name within a word: `${name}suffix` is one
argument, even when the variable's value contains spaces. `${name} suffix`
remains two arguments. `${items}[0]` and `${items}...` retain their indexing and
expansion meanings.

Variable-prefixed executable paths such as `$tools/echo` also work. Write
`$a / $b` for division; `$a/file` is a path. Variable-prefixed glob patterns
such as `$root/*.txt` interpolate the prefix before expanding the pattern.
Characters from the variable's value remain literal, including `[` or `*` in a
directory name.

Unquoted `*`, `?`, and bracket patterns in paths such as `./[ab]` expand matching paths in
sorted order; `**` supports recursive matching. Hidden entries require an explicit
leading dot, `.` and `..` are excluded, and no matches is an error. Quotes preserve
a glob as text.

```text
let $sources = src/language/*.rs
echo $sources...
```

## Anonymous functions and closures

`fn ($parameter: Type): ReturnType { ... }` creates a function value. Pass it
directly as an argument, store it in a variable or collection, or return it from
another function:

```text
fn foo($func: fn(Integer): String)
{
    echo ($func 7)
}

foo fn ($x: Integer): String
    {
        "This is a string with an integer ${x}!"
    }
```

Creating or copying an anonymous function does not run its body. Call a stored
function with `$func arguments`, or `($func)` for a zero-argument call used as
an expression. Anonymous functions use the same parameter, return, optional,
and variadic rules as named functions. Their annotations can be omitted; an
unannotated function definition has unrestricted parameters and returns, whereas
a function **prototype** with an omitted return type requires `()`.

Closures capture live variable bindings where they are created. Captured locals
remain available after the outer function returns, and writes remain visible to
other closures sharing those bindings:

```text
fn counter($n: Integer): fn(): Integer
{
    fn (): Integer
        {
            $n = $n + 1
            $n
        }
}

let $next = counter 0
echo ($next) ($next)             # 1 2
```

Each call has its own parameters and local declarations. Caller-local variables
do not replace captured bindings. Assignment updates a binding; a new `let`
declaration shadows it without changing an existing closure's capture. Closures
also preserve a captured method's live `$self` receiver, their defining module,
and the function versions visible when their code was compiled.

Anonymous functions satisfy compatible function prototypes, including named
prototype types, with the usual argument and return checks. Each evaluation of a
function literal creates a distinct callable identity; copying it preserves that
identity. Function values can be hash keys and are distinct from their display
text, `<anonymous>`.

## Functions, calls, and return values

Functions have named parameters and local variable scopes. Call them like
commands. Arguments are evaluated left to right and **retain their types** when
passed to Shelly functions, including enums, arrays, maps, and executable markers.
External commands and builtins receive text. Alias defaults and command-line
`$args` remain strings. Arrays passed to a function remain one array argument
unless expanded with `...`.

```text
fn foo($a)
{
    2048 * $a
}

let $y = foo 3
echo $y                     # 6144
echo (foo 3)                # 6144
echo (foo 3) + 1            # 6145
```

Parameters and return values can also be annotated:

```text
struct Item { value: Number }

fn make_item($value: Number): Item
{
    Item(value: $value)
}

let $item: Item = make_item 42
echo $item.value
```

Parameter types are checked before the function body runs and remain constraints
on assignments to those parameters. The return annotation applies to both explicit
and implicit returns. Bare `return` and fallthrough returning `()` require a type
that accepts `None`, such as `optional Number` or `any`. Unannotated parameters and
returns remain unrestricted. Integer arguments widen to Float where required;
other implicit conversions are rejected.

Trailing parameters annotated `optional T` may be omitted from right to left:

```text
fn describe($value: Number, $label: optional String, $limit: optional Number)
{
    echo $value $label $limit
}

describe 7                       # 7 () ()
describe 7 "items"               # 7 items ()
describe 7 "items" 10            # 7 items 10
```

Required parameters cannot follow optional ones. Use `optional any` for an
omittable parameter accepting any value. Explicit `()` occupies its argument
position, so `describe 7 () 10` skips the label while supplying the limit.
The compiler emits parameter-binding instructions containing `()` defaults;
these apply equally to direct calls, aliases, executable variables, and calls
whose arguments are expanded with `...`.

The final parameter may collect extra arguments into an array. `$rest...` accepts
elements of any type; `$rest: Number...` binds a `[Number]`:

```text
fn collect($rest...): Array
{
    return $rest
}

fn sum($values: Number...): Number
{
    let $total: Number
    for $value in $values { $total = $total + $value }
    return $total
}

echo (collect "hello" 3 true)...  # hello 3 true
echo (sum 1 2 3)                 # 6
echo (sum)                       # 0
```

A variadic parameter receives `[]` when there are no remaining arguments. It may
follow required and optional parameters; fixed parameters consume their positions
first, and all remaining arguments go into the array. Use `()` explicitly to skip
an optional position before supplying variadic arguments. The suffix follows the
element type: `$rows: [Number]...` receives `[[Number]]`, and
`$values: optional Number...` receives `[optional Number]`.

The final expression or command supplies the function's result. A trailing
semicolon or newline does not discard it. `return expression` exits the current
function immediately with that value; bare `return` returns `()`.

```text
fn answer()
{
    return 2048
    echo "unreachable"
}

fn greet($name)
{
    echo "Hello, $name!"
    return
}
```

`return` outside a function is an error. Empty functions, including `fn f() {}`,
and functions ending in a declaration, assignment, alias, nested function
definition, or completed loop return `()`. Duplicate parameter names are rejected.

Parentheses evaluate one expression and preserve its result. They support nested
calls and arithmetic, but do not contain statement sequences. Missing or extra
closing parentheses are errors, including `echo foo 3)`.

A bare name has different behavior depending on its context:

| Form | Current behavior |
| --- | --- |
| `foo` as a statement | Call `foo` with no arguments; an unknown command errors. |
| `let $x = foo` | Call it if it resolves to a function, builtin, alias, or executable; otherwise store the word as text. |
| `let $x = foo a b` | Call it with arguments; an unknown command errors. |
| `echo foo` | Pass the literal word `foo`, even when it names a command. |
| `echo (foo)` | Call `foo` with no arguments, then pass its result to `echo`. |
| `echo (foo 3)` | Call `foo` with `3`, then pass its result directly to `echo`. |
| `echo "foo"` | Pass literal text. |

A backtick prefix creates a string marked executable without calling it. There is
no closing backtick. For a known Shelly function, the reference retains that
function's version, including its parameter and return constraints. Redefining
the name does not change a previously stored reference. A visible native function
also retains its registered identity, including through qualified imports.
External commands and unresolved names remain name-based references.

```text
fn answer() { 2048 }
let $call = `answer
let $copy = $call            # Copy the reference without calling it
$call                       # Call it; top-level values are not printed
echo "$call"                # answer
echo $call                  # answer
echo ($call)                # 2048
echo `answer                # answer
```

A standalone variable or a variable/string inside parentheses is called with no
arguments when its value is marked executable. In argument positions, bare names
stay literal and executable variables, collection elements, and struct data fields
pass their values without being called. Use parentheses to execute them:
`git diff` passes the subcommand name, while `git (diff)` calls `diff` first and
passes its result. This applies to arguments of external commands, Shelly functions,
and methods. Property-style methods remain implicit: `echo $items.count` evaluates
`.count`. Ordinary string values stay text. Direct call results and nested groups
do not cause the returned value to be called a second time.

A backtick argument passes an executable reference. Grouping changes this:
``echo (`answer)`` calls `answer`. A backtick-prefixed name cannot
be the head of a grouped call with arguments: ``echo (`foo 3)`` is an error.
To pass a stored reference without calling it, use `$call`. Use `"$call"`
to pass ordinary text, or `` `$call `` to explicitly mark the value executable.
The backtick-variable form reads the value and marks its name
executable, so it can also create a reference from a stored ordinary string.

Calls through variables with arguments work as statements, in assignments and
returns, and inside parentheses:

```text
let $call = `foo
let $result = $call 3
echo ($call 4)              # 8192
```

An explicit call with arguments also accepts an ordinary string variable as its
command name. The executable marker controls implicit zero-argument calls; an
explicit call does not require that check.

Function definitions are registered before executing the submitted source, so
forward calls work. Once a function is known, compiled calls and references retain
that version, including across later redefinitions in the same submission.
Forward references with no known version resolve through that submission's
completed function namespace. Later submissions cannot change that namespace.
Redefinitions do not retarget existing direct, recursive, or sibling calls;
newly compiled code sees the new definitions. Stored backtick
references also retain their original versions when copied, passed to functions,
returned, or stored in array elements, map values, and struct fields. A bound
function reference is not redirected by an alias added later.

Functions can contain helper functions:

```text
fn welcome($name)
{
    fn say_hello()
    {
        echo "Welcome to Shelly, $name!"
    }
    say_hello
}
welcome 'world'
```

Variable lookup searches active block and call scopes; assignment updates the nearest
visible binding. This currently gives variables dynamic caller scope. Nested
function names follow their containing function blocks. `let` creates or replaces
a binding in the current scope. The initializer runs before the binding is
replaced, so `let $x = $x + 1` can read the old value. An initializer error does
not overwrite the binding with an empty value.

## Scoped code blocks

Standalone `{ ... }` blocks create variable scopes and may be nested, both at the
top level and inside functions. `let` creates a binding local to the block;
assignment without `let` updates the nearest visible binding.

```text
let $x = 1
{
    let $x = $x + 1
    { let $x = 3; echo $x }  # 3
    echo $x                 # 2
}
echo $x                     # 1
{ $x = 4 }
echo $x                     # 4
```

Returns and runtime errors unwind any active block scopes. `return` inside a
block exits the enclosing function; it remains an error at the top level. A block at the end
of a function supplies its last expression as the implicit return value, including
through nested blocks. An empty final block, or one ending in a declaration,
supplies `()`.

Blocks are statements; `{ ... }` is not yet an expression for assignments or
command arguments. Blocks scope variables; function definitions retain their
existing hoisting into the enclosing function or top level, and aliases remain
global.

## Conditional expressions

`if condition { ... }`, `else if condition { ... }`, and `else { ... }` form a
chain. Every branch requires a block with its own variable scope. Conditions use
the same boolean conversion as `!`, `&&`, and `||`. They are evaluated in order;
only the first matching branch runs, and later conditions are skipped. Newlines
and comments may separate a condition from its block or one branch from the next.

```text
let $count = 2
let $message = if $count == 0
{
    "empty"
}
else if $count == 1
{
    "one item"
}
else
{
    let $description = "several items"
    $description
}
echo $message               # several items
echo (if true { 10 } else { 20 }) + 1   # 11
```

`if` is an expression: use it in assignments, arguments, arithmetic, returns,
or other conditions. Its value is the selected block's last expression. An empty
block, a block ending in a declaration, or an unmatched chain without `else`
evaluates to `()`. A final `if` expression supplies a function's implicit return
value. `return` inside a branch still exits the enclosing function, unwinding
the branch scope.

Conditions also accept command calls, for example
`if /usr/bin/true { echo "success" }`. Exit status zero is true; nonzero or
signaled results are false. Use parentheses when combining calls with operators:
`if (check 3) && (check 4) { ... }`. The condition is evaluated in the surrounding
scope; branch-local bindings do not escape. Branch blocks follow the function
hoisting and alias rules described above.

All branches must parse, including those skipped at runtime. `else` must belong
to the same chain; a semicolon ends the chain, so put `else` after the closing
brace or on the next line, without a separating semicolon. To pass the literal
word `if` as a command argument, quote it.

## Runtime types and type guards

Every value has a read-only `.type` property returning a `Type` value. Type names
are values in expression positions, so they can be compared, stored, returned,
and used as match patterns:

```text
let $expected: Type = Integer
let $foo: any = 1024
echo ($foo.type == $expected)     # true

match $foo.type
{
    Integer =>
        {
            let $number: Integer = $foo
            echo $number
        },
    Float =>
        {
            let $number: Float = $foo
            echo $number
        }
    _ => { echo "another type" }
}

if    $foo.type == Integer
   && $foo == 1024
{
    let $number: Integer = $foo
}
```

Type equality compares exact identities. `7` has type `Integer`; `7.0` has type
`Float`. Neither has type `Number` or `any`, although those annotations accept
them. A value wrapped in `type Count = Integer` has type `Count`, and remains
distinct from `Integer`. Structs, enums, and imported types retain their defining
identities. Redeclaring a type creates a new identity; existing values and stored
type references keep the previous one. Type values are distinct from strings
such as `"Integer"`; `String($foo.type)` returns the display name.

Direct variable type guards narrow the variable inside the selected `match` arm,
`if` branch, or `while`/`until` body. The compiler also follows `==`, `!=`, `!`,
short-circuit `&&` and `||`, and remaining alternatives in `else` and `_` branches.
For example, the right side of `&&` above sees `$foo` as an Integer. Incompatible
annotations, returns, and fields in a narrowed branch are compile errors.

Narrowing does not change the variable's declared assignment constraint or
convert its value. Assignments, calls, and control-flow joins discard facts that
may have changed. Closures check captured mutable values again when called;
creation inside a guard does not permanently narrow a live capture. Runtime type
checks still enforce annotations on writes and calls.

Ordinary arrays report `Array` and maps report `HashMap`; a guard retains any
stronger element/key/value constraints already known by the compiler. Named
collection types report their own distinct identity. Type values can themselves
be collection elements and hash keys, and require an initializer when annotated
as `Type`. The property name `type` is reserved for metadata and cannot be declared
as a struct field or method, or assigned through `.type`.

Bare command arguments keep their word semantics: `foo Integer` passes the word
`Integer`. Use `foo (Integer)` or a variable to pass a Type value. Existing
`Integer(value)` and other conversion expressions keep their conversion behavior.

## Match expressions

`match` evaluates a subject once and tries arm expressions from top to bottom.
The first matching arm runs; its block supplies the result:

```text
let $description = match $value
    {
        0            => { "zero" }
        1..10        => { "one through nine" }
        $expected    => { "the expected value" }
        $valid_range => { "inside the configured range" }
        $a..$b       => { "inside the other configured range" }
        _            => { "something else" }
    }
```

Non-range arm values use the same equality as `==`, including literal expressions,
arrays, maps, structs, and enum values. Variables supply their current values;
they do not introduce bindings. Arm expressions are evaluated only when reached,
and later expressions and bodies are skipped after a match. Function calls and
arithmetic can supply the subject, an arm value, or range bounds.

A range-valued arm tests **integer membership**. `a..b` excludes `b`, `a..=b`
includes it, and omitted bounds are unbounded. Reversed or empty ranges match
nothing. Non-integer subjects do not match range arms; other arms can handle them.
A variable holding a range has exactly the same behavior as a written range.

A bare `_` is an optional fallback and must be last; `"_"` is an ordinary string
pattern. Every arm requires a block. Commas after arm blocks are optional. An empty
arm block returns `()`. An empty arm list is a syntax error. If no arm matches and
there is no fallback, execution raises `Match error: No arm matched the value.`

Arm blocks have the same variable scopes, function hoisting, and alias rules as
other blocks. `return` exits the enclosing function; `break` and `continue` target
the enclosing loop. All arms are parsed and type-checked, even when not selected.
Match expressions also work in assignments, returns, collections, and grouped
command arguments.

## Loops

All loops require a block and are statements. Body results are discarded; a
function or conditional branch ending in a completed loop returns `()`.
Each iteration creates a fresh scope for bindings and body-local variables.
These can shadow outer variables; assignment still updates the nearest visible
binding. Body-local bindings do not escape. Loops may nest in any combination.

`return` exits the enclosing function, and runtime errors stop execution. Both
clean up active loop scopes and iterators. Function definitions and aliases inside
loops follow the hoisting and global-alias rules for blocks.

### For

Use one binding per yielded item, or destructure a yielded array into multiple
bindings. Arrays yield their elements, ranges yield integers, and maps yield
`[key, value]` pairs. The expression after `in` can be a literal, a variable, an
indexed value, a conditional, or a function call:

```text
for $index in 1..4
{
    echo $index              # 1, then 2, then 3
}

let $items = ["red", "green", "blue"]
for $value in $items { echo $value }
for $value in [10, 20] { echo $value }

let $settings = ["width": 80, "height": 24]
for $key, $value in $settings { echo $key $value }
for $key, $value in ["answer": 42] { echo $key $value }
```

A parenthesized binding list destructures each yielded array. This works with
map entries, arrays returned by `.zip`, and custom iterators:

```text
for ($test_file, $output_file) in $tests.zip $outputs
{
    echo $test_file $output_file
}

for ($x, $y, $z) in [[1, 2, 3], [4, 5, 6]] { echo $x $y $z }
```

Each element must be an array with exactly as many elements as the pattern has
bindings. The check happens before binding any values or entering the loop body;
a mismatch reports the loop's source location. Patterns are flat lists of one
or more distinct variables; trailing commas, newlines, and comments are allowed.
`for ($value,) in [[1], [2]]` unwraps each one-element array. An empty iterable
skips the body without attempting to destructure an element.

The iterable is evaluated once, before any loop bindings are created. Array
elements retain their types and are visited in array order. Maps visit each
key/value pair once in unspecified order; keys use the map's canonical value
representation. Iteration uses a snapshot: assigning to the source collection or
mutating it during the loop does not change the remaining iterations. Changing
a collection held by a loop binding also leaves the original element unchanged.

Range iteration is lazy and requires both integer bounds. `..` excludes the end,
`..=` includes it, and reversed ranges are empty. Empty arrays, maps, and ranges
skip the body. A map yields one `[key, value]` array per step, so either a single
binding or `for ($key, $value) in $map` works. The existing unparenthesized
`for $key, $value in $map` syntax remains supported and also destructures array
items from other iterators. Duplicate binding names are errors.

`for` uses the `next_item` protocol. Any type can define a zero-argument method
whose declared return type is `T | ()`:

```shy
struct Counter { remaining: Integer }

fn Counter::next_item(): Integer | ()
{
    if $self.remaining == 0 { return () }
    let $item = $self.remaining
    $self.remaining = $self.remaining - 1
    $item
}

let $counter = Counter(remaining: 3)
for $item in $counter { echo $item }  # 3, 2, 1
echo $counter.remaining             # Still 3
```

The iterable is evaluated once and copied into a private receiver. Each step
calls that receiver's `next_item` method. Changes to `$self` advance only that
private value, so repeated and nested loops over the source have independent
state. The method version is captured when the loop is compiled, following the
same rules as other method calls. Methods can still perform ordinary external
side effects or modify other variables in their defining scope.

Returning `()` ends the loop; every other value is yielded, including `false`,
zero, empty strings, and empty arrays. Consequently an array containing `()`
ends iteration at that element. To yield a unit value as data, wrap it in an
array or struct. A map entry whose value is `()` remains a valid two-element
array and does not end iteration.

Arrays, argument expansions, hash maps, and bounded ranges provide native
`next_item` methods. Their signatures reflect the collection's contents:

| Receiver type | `next_item` return type |
| --- | --- |
| `[Integer]` | `Integer \| ()` |
| `[String: Integer]` | `[String, Integer] \| ()` |
| `Range` | `Integer \| ()` |
| Unspecified array or argument expansion | `any \| ()` |
| Unspecified hash map | `[any, any] \| ()` |

Explicit annotations supply element types; otherwise homogeneous contents are
inferred. Empty, mixed, or unknown contents use `any`. Hash keys and values are
inferred independently, and nested collection types retain their inner types.
Inference does not convert mixed integers and floats or constrain an unannotated
variable's later assignments.

```shy
let $scores = ["alice": 10, "bob": 20]  # Inferred [String: Integer]
for ($name, $score) in $scores
{
    let $points: Integer = $score
    echo $name $points
}
```

The checker carries these item types into loop bindings, including destructured
map pairs and annotated array shapes. It also reads existing collection types
and contents when compiling a later REPL input. Inferred types are discarded
where writes, calls, or control flow make them uncertain; explicit annotations
remain authoritative. User overrides are never assumed to have a native method's
signature, and dynamic values still receive runtime checks.

Native loops use efficient private cursors, and ranges remain lazy.
User-defined overrides participate in
the same protocol. A type without `next_item` produces an iteration error;
methods with parameters or a return type lacking `| ()` are rejected.

A direct call such as `$items.next_item` advances the actual receiver: it removes
the first array element or one map entry, or advances a range's start. `for`
advances a private copy instead. A saved method reference such as
``let $next = `$items.next_item`` retains the normal live-receiver behavior.

Fixed-length array annotations describe each position's type, making structured
yields explicit:

```shy
struct Pairs { remaining: Integer }

fn Pairs::next_item(): [Integer, String] | ()
{
    if $self.remaining == 0 { return () }
    $self.remaining = $self.remaining - 1
    [$self.remaining, "item"]
}

for ($index, $label) in Pairs(remaining: 2) { echo $index $label }
```

`[Integer, String]` requires exactly two elements with those respective types.
`[Integer,]` requires exactly one element; `[Integer]` remains a homogeneous array
of any length. Fixed-length annotations can be nested and used for variables,
parameters, returns, fields, and collections. Numeric widening applies to each
position. Wrong lengths or element types fail validation. Destructuring still
checks the actual yielded array before creating bindings.

`T | ()` is another spelling of `optional T`, including in other annotations.
General unions are supported too: an iterator can return `Integer | String | ()`.
Existing `optional T` iterator return annotations are equivalent.

### Unbounded loop

`loop { ... }` repeats its required block without a condition or iterable:

```text
let $count = 0
loop
{
    $count = $count + 1
    if $count == 2 { continue }
    echo $count
    if $count == 3 { break }
}
# Prints 1, then 3
```

An empty `loop {}` runs indefinitely.

### While and until

`while condition { ... }` repeats while the condition converts to true.
`until condition { ... }` repeats while it converts to false. Both check the
condition before the first iteration and before every subsequent iteration:

```text
let $n = 0
while $n != 3
{
    echo $n                  # 0, 1, 2
    $n = $n + 1
}
until $n == 0
{
    echo $n                  # 3, 2, 1
    $n = $n - 1
}
```

Conditions accept the same expressions and command calls as `if`, with the same
boolean conversion and short-circuit rules. A command's zero exit status is true;
nonzero status is false. `while false { ... }` and `until true { ... }` skip their
bodies, although those bodies must still contain valid syntax. A block is always
required, and newlines/comments may separate the condition from its opening brace.

The condition runs in the surrounding scope, before the body scope is created.
`continue` rechecks it; `break` exits without evaluating it again.

### Break and continue

`break` leaves the innermost active loop; `continue` skips the remainder of its
current iteration. In `for`, it advances to the next element; in `loop`, it
restarts the body; in `while` and `until`, it rechecks the condition. Neither
accepts a value or a loop label:

```text
for $i in 0..5
{
    if $i == 1 { continue }
    if $i == 3 { break }
    echo $i                  # 0, then 2
}
```

Both statements unwind the current iteration's scopes, including nested blocks,
and discard abandoned expression temporaries. Executing either without an active
loop in the current function or top-level execution produces an error. A called
function cannot control its caller's loop. Skipped branches still require valid
syntax, but do not execute their loop-control statements.

## Processes, aliases, and environment

`cd PATH` changes directory and returns `ExecResult(0)` on success or
`ExecResult(1)` on failure, so it can be used directly as a condition.
`exit` stops execution with status zero;
`exit 7` stops it with status 7. An explicit exit status must be an integer from
0 to 255. `echo` and the other Unix commands in these examples are external
programs found through `PATH`.

External commands return an `ExecResult` status. Their stdout is inherited by
Shelly unless redirected; assigning a command result does **not** capture its printed output.

```text
let $status = /usr/bin/false
echo $status                # ExecResult(1)
```

Assignment can retain a failed status as a value. An uncaptured failing command
stops the remaining submitted source; an intermediate failing command also stops
a function. The interactive REPL reports the error and accepts another input.
A failing noninteractive script exits unsuccessfully.

Aliases prepend fixed arguments. Alias arguments are stored literally, without
variable interpolation or automatic calls. Aliases are global even when declared
inside a function. A newline, semicolon, or closing function brace ends an alias
definition.

```text
alias say = echo "prefix"
say 'hello'                 # prefix hello
```

Resolution expands aliases, then checks builtins, functions, and external
programs, in that order. An alias can add defaults to its own command name;
indirect alias cycles produce an error.

Shelly imports the environment. New variables are private unless declared with
`let export`; only exported variables reach child processes.

```text
let export $SHELLY_PROJECT = 'shelly'
```

Useful predefined variables include `$args`, `$pid`, `$pwd`, `$HOSTNAME`, `$HOME`, `$PATH`,
`$shelly` (an executable reference to this binary), `$version`, `$os` (also `$OS`),
`$build_date`, `$build_time`, `$interactive`, `$login`, and `$rc_path`.
`$pid` is the running Shelly process's ID as an `Integer`.
`$last_cmd_time` is the last REPL command's formatted elapsed time as a `String`.
`$os` identifies the host operating system, for example `"macos"` or `"linux"`,
and can be used in functions to select platform-specific commands.
`$rc_path` is the configured init path, `<not found>` when missing, or
`<unloaded>` when init loading is disabled.

Each module also starts with an empty `$widgets: [WidgetFn]` array, where
`WidgetFn` is a predefined distinct type equivalent to `type WidgetFn = fn(): String`.
Assign named function references or closures to the array; each callback takes no
arguments and returns a `String`.

```text
$widgets = [fn (): String { "ready" }]
for $widget in $widgets { echo ($widget) }
```

### File and variable redirection

`->` redirects stdout, `~->` redirects stderr, and `~+->` sends both streams to
the same destination. Data flows from left to right. These operators can be
combined on one command:

```text
let $output: String
let $errors: String
input.txt -> $output
sh -c 'echo output; echo error >&2' -> $output ~-> $errors
let $status = sh -c 'echo failed >&2; exit 1' ~+-> $output
echo $status                         # ExecResult(1)
```

A bare variable on the right of an output operator receives text; declare it
first, with a type that accepts `String`. Captures preserve all UTF-8 text,
including trailing newlines. Invalid UTF-8 produces an error. File-to-file and
process-to-file transfers preserve arbitrary bytes. Capturing output does not
change the command's return value or its normal failure handling.

Other destinations are file paths, created or truncated when opened. Quote a
variable to use its value as a filename instead of capturing into the variable:

```text
echo hello -> output.txt
let $log = 'command.log'
sh -c 'echo output; echo error >&2' ~+-> "$log"
```

A file can also supply text directly to a variable. A variable used as this
source holds the filename, matching `test.shy`:

```text
let $contents: String
input.txt -> $contents
let $path = 'input.txt'
$path -> $contents
```

A bare source word that resolves to a command runs that command; otherwise it
names a file. Quote the source path to force file access when its name matches a
command. Redirections also apply to commands called inside Shelly functions and
to builtin diagnostics. Streams are restored on completion, errors, and returns.
Each stream may be redirected once per expression. Left-facing redirection
operators are not supported. Input from files or variables into a process will
use `|` pipelines, which are not implemented yet. Append redirection is also
not implemented.

## Supervised processes and terminals

`run_process` accepts an argument array and an options map. It returns a result
map rather than raising a language error for a nonzero exit, signal, timeout, or
launch failure. Inspect the result explicitly:

```text
let $result = run_process ["/usr/bin/cat"] [
    "stdin_file": "input.bin", "stdout_file": "output.bin",
    "stderr_file": "errors.txt", "timeout_ms": 3000,
]
echo $result["exit_code"] $result["signal"] $result["timed_out"] $result["error"]
```

Options are `cwd`, `env`, `stdin_file`, `stdout_file`, `stderr_file`, and
`timeout_ms`. Arguments, paths, and environment names/values must be strings.
An omitted `env` inherits Shelly's exported variables; an explicit map replaces
the environment, including `[:]` to clear it. File paths are relative to the
calling shell's directory, independently of the child's `cwd`. Output files are
truncated; use distinct files for separate streams. Omitted streams inherit the
current streams, including Shelly's `->`, `~->`, and `~+->` redirections. Explicit
file options override that inheritance. File I/O preserves arbitrary bytes.

Results always contain `exit_code` (integer or `()`), `signal` (integer or `()`),
`timed_out` (boolean), and `error` (launch/I/O error text or `()`). Invalid API
arguments are language errors. Deadlines are nonnegative integer milliseconds;
omitting `timeout_ms` waits indefinitely. On timeout Shelly sends SIGTERM to the
child's process group, waits 100 ms, then sends SIGKILL and reaps the child. It
also stops remaining group members after the direct child exits normally.
Children that deliberately create a different process group escape this group
cleanup. This API currently requires Unix.

The native terminal API creates a child with a controlling PTY:

```text
let $terminal: Terminal = open_terminal ["/usr/bin/cat"] ["rows": 40, "columns": 140]
terminal_write $terminal "hello\n"
let $reply = terminal_read $terminal 1000
echo $reply["text"] $reply["eof"] $reply["timed_out"]
let $status = terminal_close $terminal
```

`open_terminal` accepts `cwd`, `env`, `rows`, and `columns`. Dimensions must be
integers from 1 to 65535; defaults are 40 by 140. `Terminal` is an opaque shared
handle with identity equality; assigning it shares the same terminal. Reads
return UTF-8 text, EOF/timeout flags, and the process-result fields above. A read
timeout does not kill the process or imply EOF. Reads may return partial output;
UTF-8 sequences split between reads are retained, while invalid or truncated
UTF-8 raises an error. Writes accept strings and have a five-second deadline if
the PTY stops accepting input. Closing releases the PTY, stops the process group,
and reaps the child; repeated closes return the same status. Dropping the last
handle also cleans up. Writes and reads after explicit close are errors.

Strings provide `$text.chars`, `$text.contains $part`, `$text.starts_with $prefix`,
`$text.ends_with $suffix`, `$text.replace $old $new`, `$text.split $separator`,
`$text.trim`, `$text.trim_start`, and `$text.trim_end`. They return new values and
never mutate the receiver. `chars` returns Unicode scalar strings; `split`
retains empty fields, including leading/trailing ones. Trimming uses Unicode
whitespace and is always explicit: captures still preserve trailing newlines.

## Modules

Module declarations are private by default. Prefix a declaration with `pub` to
make it available to importers, both through qualified names and selected imports:

```shy
# Example module: counter.shy
let $state = 0
fn increment(): Integer { $state = $state + 1; $state }

pub fn next(): Integer { increment }
pub let $label: String = "counter"
pub type Count = Integer
pub struct Snapshot { count: Count }
pub enum Status { Ready, Done }
pub fn Snapshot::read(): Count { $self.count }
pub alias advance = increment
```

Private names remain available inside their defining module, including in public
functions, methods, and aliases. `pub` is allowed only at module top level, including
declarations inside an enabled `[when ...]` declaration block. Methods are private
unless marked `pub`, even when their receiver type is public. Struct fields and
enum variants retain their existing access rules.

`pub let export $NAME = ...` makes a variable public and exports it to child
processes. Plain `let export` affects the child environment only; it does not make
the variable accessible through imports.

Imports are top-level declarations:

```shy
import foo
import net::{ ping }
import config::{ $enabled, Settings }
import platform when $OS == "linux"

foo::run "argument"
echo $foo::value
let $settings: config::Settings = config::Settings(enabled: $enabled)
```

`import foo` searches for `foo.shy` beside the importing file, then searches
the directories in `$SHELLY_MODULE_PATH`, in order. Set that variable in the
environment or an earlier REPL submission; it is a colon-separated String.
For command-line source and REPL input, the first search directory is the current
directory. Modules are cached by canonical path and initialized once per
interpreter. Circular imports are errors.

Only public names listed in `::{ ... }` enter the enclosing scope. Other public
variables, functions, types, structs, enums, and aliases remain accessible through the module
namespace: `$foo::value`, `foo::run`, `foo::Point`, and `foo::State::Ready`.
Selected imports clone the original object's `Rc` into the enclosing scope map.
They preserve type and function identity, and assigning through an imported
variable updates the module's binding. A new `let` declaration creates a separate
local binding. Reimporting the same object is allowed; importing a different
object over an existing name is an error. Public aliases retain their defining
module when resolving their targets.

Imports are private too. Re-export selected names with `pub import`, or publish
an entire module namespace with an unselected `pub import`:

```shy
import implementation::{ helper }       # Local access only.
pub import counter::{ next, Count }     # Export next and Count, not counter's namespace.
pub import net                         # Export the net namespace.
```

An importing script can then use `bridge::next`, `bridge::Count`, and
`bridge::net::ping`. It cannot access `bridge::implementation::helper` or
`bridge::counter::next`. Implicit prelude bindings are local to each module;
re-export a prelude type explicitly when it belongs in that module's interface.

Imported functions execute with their defining module's variables and functions.
Qualified function calls follow ordinary argument rules: `echo foo::read` passes
the name as text; `echo (foo::read)` calls it. A backtick, as in `` `foo::read ``,
keeps a callable reference.

Native registration and module visibility are separate. Rust registers a type or
function once; its registration can be `NativeVisibility::Visible` or
`NativeVisibility::Hidden`. Visible registrations seed each module's local names.
Hidden registrations remain available internally without appearing in module name
lookup. Existing core native registrations remain visible by default.

The `visible` builtin accepts one function reference or type name. It makes the
symbol available locally; `pub visible` also adds it to the current module's exports:

```shy
# Example module: std/process_tools.shy
visible "Terminal"                 # Local type for implementation signatures.
visible `run_process               # Local native function used by wrappers.
pub visible `open_terminal         # Native function exposed to importers.

pub fn run($command: Array, $options: HashMap): HashMap
{
    run_process $command $options
}
```

An importer can call `std::process_tools::run` and
`std::process_tools::open_terminal`, or select those names with `::{ ... }`.
The module does not export `Terminal` or `run_process` in this example. To expose
a type as well, use `pub visible "Terminal"`. Scripted definitions continue
to follow the normal module export rules, so the wrapper and the native function
share one public interface.

For a hidden native function, use a backtick reference such as ``visible `native_fn``.
For a hidden native type, `visible "NativeType"` looks directly in the native
registration table. Plain strings name types, not functions. Qualified references
and type names work too: ``pub visible `other::helper`` and
`pub visible "other::Type"` publish their unqualified names in the current
module. Function references stored in variables are accepted. External commands
and bound methods are not module function declarations and cannot be published.

Literal top-level visibility declarations are processed before type checking, so
annotations and wrappers throughout the same file can use native implementation
types. Declarations inside excluded `[when false]` blocks are skipped. Computed
arguments take effect at runtime. Local `visible` calls inside functions or ordinary
blocks also take effect at runtime; `pub visible` requires module top level.
New type names are then usable by subsequent compilations. Failed compilation
rolls back the declarations prepared for that submission.

Visibility changes apply to the current module. Hidden registrations do not
become visible in unrelated modules; importers see only exported names. Repeated
publication of the same object is allowed, conflicting bindings are errors, and
calling local `visible` does not revoke an existing export. `visible --export` is
replaced by `pub visible`. Native function references and imported types retain
their original identities through
imports and re-exports.

On the Rust side, `NativeFunction::new(name, visibility, body)` creates a native
function registration, and `TypeRegistry::register_native(name, kind, visibility)`
registers a native type. The registry retains hidden entries while new module
scopes copy only initially visible names. This lets a future `std/json.shy`
select native JSON exports with `pub visible` and implement its remaining
interface in Shelly; a native JSON API is not implemented yet.

Standard-library imports use a separate search path:

```shy
import std::foo
import std::net::{ ping }
import std::foo when $OS == "linux"

std::net::ping "host"
```

`std::net` resolves to `net.shy` directly inside a standard-library directory.
It never searches beside the importing file or through `$SHELLY_MODULE_PATH`.
`$SHELLY_STD_PATH` is a colon-separated String; a set value replaces all default
search directories, including when it is empty. When unset, the directories are:

```text
/etc/shelly/std:/usr/local/share/shelly/std:/usr/share/shelly/std
```

Search proceeds left to right; the first matching module wins. An error in that
module is reported instead of trying a later copy. Selection happens per module,
so different modules can come from different directories in the same search path.
Empty path entries are skipped. To put a development library before the installed
library while retaining fallback directories, include them explicitly:

```shy
let export $SHELLY_STD_PATH = "/home/me/workdir/shelly/std:/etc/shelly/std:/usr/local/share/shelly/std:/usr/share/shelly/std"
```

Qualified access retains the `std::` prefix, including variables
(`$std::foo::value`) and types (`std::foo::Type`).

Shelly selects `std::prelude` by searching for `prelude.shy` using the same
standard-library search path. Its public types are automatically available
without qualification in the main scope and subsequently loaded modules. These
imports share the original type handles and preserve type identity. Prelude
functions and variables remain qualified unless explicitly selected:

```shy
import std::prelude::{ helper, $setting }
```

Startup runs in this order:

| Stage | When it runs | Purpose |
| --- | --- | --- |
| `/etc/shelly/profile.shy` | Login shells (`-l` or `--login`) | System-wide environment and library selection. |
| `~/.shelly_profile.shy` | Login shells, after the system profile | User environment and overrides. |
| Initial prelude load | All modes, unless already loaded by a profile | Select and execute the prelude once. |
| `~/.shelly_init.shy` or `--rcfile PATH` | Interactive shells, unless `--norc` | User configuration with prelude types available. |
| User input or script | After startup | Execute commands with the selected prelude. |

Missing profile files are skipped. Profiles initially have no implicit prelude;
they can configure its location before it loads. If the system profile explicitly
loads it, the user profile also has those types available. Non-login shells skip
both profiles and select the library from their inherited environment.
Noninteractive script, `-c`, and stdin modes still load the prelude, but skip the
interactive init file. `--norc` skips only that init file, not login profiles or
the prelude. Configure a terminal to launch `shelly --login` when its sessions
should read the profiles.

For example, a system administrator can put this in `/etc/shelly/profile.shy`
to select the machine's standard library:

```shy
let export $SHELLY_STD_PATH = /home/me/workdir/shelly/std
```

With no explicit reload, Shelly waits until both profiles have run before
selecting the prelude. The user can therefore set a different path in
`~/.shelly_profile.shy` before that first load. `let export` also passes the
configured path to child processes, including non-login Shelly instances.
Each new interpreter loads its own prelude; the cache is not shared across
processes.

To load the chosen prelude immediately for following startup scripts, add the
zero-argument builtin `prelude_reload` to the profile:

```shy
# /etc/shelly/profile.shy
let export $SHELLY_STD_PATH = /home/me/workdir/shelly/std
prelude_reload
```

A successful profile reload satisfies startup's initial load, so Shelly does not
execute it again before the RC. If a later user profile or RC chooses another
library, it must call `prelude_reload` after setting the path to replace the
already selected prelude. The same sequence works at the REPL.

Changing `$SHELLY_STD_PATH` alone changes subsequent uncached standard-library
lookups; it does not rerun the prelude or replace cached modules. The selected
prelude remains cached even if its source file is removed. Explicit
`import std::prelude` reuses the current scope's cached generation.
If no prelude exists during initial discovery, execution continues without one
and Shelly does not automatically search again. Errors in a prelude found during
automatic initialization stop startup. The prelude does not implicitly import
itself; its dependencies bootstrap before its exported types are distributed.

`prelude_reload` searches the current `$SHELLY_STD_PATH`, or the defaults when
unset, and executes the selected `prelude.shy` again.

A successful reload replaces the implicit prelude types and `std::prelude`
namespace in the current module scope, and supplies that generation to future
modules. Previously loaded modules retain their bindings. Existing values,
function references, and explicitly selected functions or variables keep their
original identities. Old implicit type names absent from the new prelude are
removed; conflicting local types cause an error. Reloading the same file still
creates new nominal types. Dependencies already loaded remain cached.

New type bindings apply to subsequent compilations: the next REPL input, startup
script, or newly loaded module. A script's imports and type references are
resolved before its body runs, so a reload in that body cannot retroactively
change them.

Unlike optional startup discovery, an explicit reload reports a missing prelude.
Lookup, parse, evaluation, and binding conflicts preserve the previous prelude
bindings. Script side effects, such as output or file writes, cannot be undone.
Recursive reloads during prelude loading are rejected.

All `when` conditions are evaluated before loading the containing file's explicit
imports or compiling its body. They see the existing caller environment, including
prelude types and prior REPL submissions, and cannot use names declared or imported
in the same file.
A false condition skips file lookup and initialization entirely. After the
conditions are evaluated, enabled imports load in source order, then the body
compiles and runs. Import placement does not delay loading until execution
reaches that line.

## Conditional declarations

Attach `[when expression]` to a function or a declaration block to select code
before compilation:

```shy
[when $os == "linux"]
fn os_gadget()
{
    echo "Linux implementation"
}

[when $os == "macos"]
{
    fn os_gadget() { echo "macOS implementation" }
    let $platform_label = "macOS"
}
```

The AST retains each condition until the compilation prepass evaluates it using
Shelly's normal truthiness rules. A false condition removes the entire function
or block before import resolution, type checking, and function registration.
Evaluation errors are reported; they are not treated as false. Excluded source
must still be syntactically valid, but may refer to unavailable types or modules.
Nested conditions inside excluded code are not evaluated.

An included annotated block inserts its contents into the surrounding scope.
Its variables, types, and functions remain visible afterward, and its statements
execute in their original position. Ordinary unannotated blocks retain their
usual scope. To conditionally declare a struct, enum, variable, or import, put it
inside an annotated block.

Like `import ... when`, exclusion conditions use the environment from before the
containing file or REPL submission loads. They cannot depend on declarations or
imports in that submission, or on function parameters and runtime loop variables.
Conditions are evaluated once during compilation, including those written inside
function bodies. Prior REPL bindings and functions are available.

## Current limitations and known issues

- Variables use dynamic caller scope; function definitions are hoisted within each input.
- Command results are statuses, not captured stdout. Shell errors in noninteractive
  execution return a general failure status; only explicit `exit N` selects a specific status.
- Pipelines, append redirection, ordering comparisons (`<`, `>`, `<=`, `>=`), array slicing,
  and array append syntax are not implemented.
- Iterating or expanding a range requires both bounds. `break` cannot carry a value
  or target a named loop. Standalone blocks are statements, not general expressions.
- Some parser diagnostics contain verbose lists of attempted alternatives.

## Implementation and development

`src/main.rs` selects the execution mode. `src/runtime/repl.rs` implements the
Reedline editor, completion, and prompt. The active tokenizer, parser, AST,
compiler, values, and interpreter live under `src/language/`. Source is tokenized
and parsed into an AST, checked against a staged type registry, compiled to
bytecode, optimized, linked, then executed. The initial checking pass registers
lexically scoped enum, struct, and named type identities before resolving annotations.
It checks constructors, required-field cycles, annotations, known incompatible
initializers, assignments and returns, and known member accesses. Runtime checks
cover dynamic values, function arguments, return paths, and nested writes.
Collection and local binding inference propagates known item types into loops;
broader function and control-flow inference remains future work. Builtin types, container
constraints, unions, enums, structs, and named types share stable `TypeId`s
independent of name visibility.

Two optimization passes run before linking: adjacent `PopResult`/`PushResult`
pairs are removed, and redundant `CheckResult` instructions are dropped only
when the compiler can prove the result is already empty. The proof is conservative
across calls and control-flow boundaries.

The interpreter stores named scopes in a `HashMap<String, Scope>` and tracks the
current scope by name. Startup creates and selects the `main` scope. Loaded
modules have separate entries keyed by canonical source path; qualified names
resolve through this registry. Function calls select their defining scope and
restore the caller's scope on completion or error.

Each `Scope` owns variable bindings, the type registry, function namespaces,
and aliases. It preserves caller-scoped variables and lexical type
and function identities across submissions. Function entry and exit update the
variable scope and active function namespace together. Built-in handlers, special
variable readers, I/O state, and execution results remain on the interpreter.

Jumps and `EnterLoop` initially refer to labels. Linking resolves them to numeric
instruction indexes independently for each function and the top-level code,
rejecting missing or duplicate labels. `JumpTarget` instructions remain as landing
points, without their labels; the interpreter sees only numeric destinations.

Blocks use `EnterScope` and `ExitScope`. `EnterLoop` pushes the continue/break
addresses and saved execution depths; `ExitLoop` pops that frame. `Break` and
`Continue` unwind scopes and temporary values before jumping. For loops also use
iterator instructions that capture and advance a private `next_item` receiver;
while and until use `ToBoolean` and conditional jumps.
Loop and iterator stacks belong to the current execution frame and are cleaned
up on returns and errors.

Build with `cargo build --locked` and check behavior through command-line source,
scripts, or the REPL. `cargo clippy --locked --all-targets` runs the Rust lints.

[test.shy](test.shy) runs the Shelly test suite from the repository root:

```sh
./target/debug/shelly -m test.shy
./target/debug/shelly -m test.shy native
./target/debug/shelly -m test.shy C001
```

The suite includes 4,319 process cases, 98 stateful REPL scenarios, prompt/path
checks, watchdog probes, native API tests, and harness failure controls. All
orchestration and assertions run in Shelly; no Python, pexpect, or other shell is
needed. Standard Unix utilities still provide file operations and byte/regex
comparisons. See [tests/README.md](tests/README.md) for the case format and
[tests/MIGRATION.md](tests/MIGRATION.md) for coverage mapping.

Each case gets a private temporary fixture, isolated HOME/TMPDIR, an explicit
child environment, and native process deadlines. The runner continues after
failures and exits 1 if any test fails or the selection is empty. Normal
completion removes temporary fixtures; interrupted runs can leave directories
under `/tmp/shelly-suite.*`.

**Keep validation releases separate from an installed/default-shell binary:**

```sh
cargo build --locked --release --target-dir target/test-validation
./target/test-validation/release/shelly -m test.shy
```

This does not replace `target/release/shelly`.

## Direction

The aim is to keep the immediacy of a shell while giving larger scripts a clear
path to structure. Future work includes broader type inference and contracts,
network and JSON support, and pipelines for text and structured data.