c-shell 0.2.0

An interactive shell for C: type an expression, a statement or a function and see what your real compiler does with it
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
# c-shell

An interactive shell for the C language. To check how a piece of syntax
behaves, skip the new-file / write-`main` / compile / run loop — just type it
at the prompt.

```
c-shell 0.2.0  ·  cc (GCC) 16.1.1 (default std gnu23)
In [1]: int x = 41;
In [2]: x + 1
Out[2]: 42
In [3]: 3 / 2
Out[3]: 1
In [4]: 3.0 / 2
Out[4]: 1.5
In [5]: -1 > 0u
<input>:1:4: warning: comparison of integer expressions of different signedness
    1 | -1 > 0u
      |    ^
Out[5]: 1
```

## Not an interpreter

c-shell assembles your input into a complete C program and hands it to the
**real compiler on your machine**. That is deliberate: integer promotions,
implementation-defined details, ABI choices and compiler diagnostics are most
useful when they come from the toolchain you actually use. An interpreter's
answer speaks only for the interpreter.

A result is still an observation of one compiler invocation, not a guarantee
from the C standard. In particular, undefined or unspecified behavior may
change with compiler version, flags, surrounding code or another execution.

## Installation

Requires a Rust toolchain to build and, at runtime, a C compiler compatible
with one of the supported GNU/Clang or MSVC command-line dialects. GCC, Clang,
MSVC and tcc are the built-in detection candidates.

Install the published crate from crates.io:

```sh
cargo install c-shell
```

Or install directly from a source checkout:

```sh
cargo install --path .
```

With `--cc`, only that compiler is tried. Otherwise startup checks `$CC`, then
PATH (`cc`, `gcc`, `clang`, `tcc`; on Windows `gcc`, `clang`, `cc`,
`clang-cl`, `cl`). `--cc` and `$CC` must name one executable or path, not a
shell command containing flags. The first candidate that passes every check
wins. Capabilities are probed by trial compilation, never derived from version
strings — executable names can be aliases, and distros backport features. For
example, Apple Command Line Tools may provide `/usr/bin/gcc` as an Apple Clang driver, while
GNU GCC installed separately through Homebrew or MacPorts is genuine GCC.

```sh
c-shell                              # auto-detect a compiler
c-shell --cc clang --std c23         # request compiler and mode; verify the banner
c-shell --timeout 30                 # deadline for each compilation and program run
c-shell -e 'sizeof(long)'            # evaluate and exit: bare value on stdout,
                                     # diagnostics on stderr, exit code on failure
c-shell --script demo.csh            # run inputs from a file, then exit
c-shell --quiet                      # interactive, but skip the banner
echo '1 + 1' | c-shell               # piped input: transcript output, no banner
```

Terminal handling is automatic: when stdout is not a terminal (or
`TERM=dumb`, or `NO_COLOR` is set) colors are off; when stdin is not a
terminal the banner, prompts and farewell are suppressed and input is read
in batch mode — which also accumulates multi-line definitions, so scripts
and pipes can contain full function bodies.

The language standard defaults to **whatever your compiler defaults to**
(gnu23 for gcc 16, gnu17 for clang 22; the banner shows what was detected).
This matches the compiler's language mode, not its entire command line:
c-shell also adds diagnostics, output and platform link flags plus generated
REPL scaffolding.
Select a supported mode with `--std c17`, or switch mid-session with `%std
c17` / `%std default`. One exception: when the compiler's default mode cannot
compile `_Generic` (MSVC without `/std:` is the common case), c-shell tries
c17 and then c11, and the banner reports an automatic raise. `_Generic`
support is the actual capability floor: a candidate that cannot compile a
representative value-printer probe in the selected mode is skipped. An
unsupported explicit `--std` is an error both at startup and in `%std`.

## Usage

| Input                  | Behavior                                      |
| ---------------------- | --------------------------------------------- |
| `x + 1`                | evaluated, value printed as `Out[n]`          |
| `x + 1;`               | trailing `;` runs it silently (as in IPython) |
| `int x = 41;`          | declaration, visible to later inputs          |
| `int f(int a) { ... }` | function definition, hoisted to file scope    |
| `int main() { ... }`   | rejected with guidance; c-shell supplies main |
| `#include <time.h>`    | likewise                                      |

A completed interactive `if` uses blank-line confirmation: press Enter on the
empty continuation line to submit it, or type `else` / `else if` there to
continue the same statement. Functions and loops submit directly when their
required closing syntax is entered. A braced `struct`, `union` or `enum`
definition remains open after `}` until its mandatory declaration semicolon is
entered. Control headers with their body on the next line, mandatory
`do ... while`, and conditional preprocessor groups through `#endif` are
accumulated automatically. In scripts
and pipes, one-line lookahead attaches `else` without requiring a blank line.

c-shell generates its own `main` to host session locals and the output
protocol. Pasting a complete program such as `int main() { puts("hi"); }`
therefore produces a direct explanation instead of a compiler redefinition or
GCC nested-function warning: enter the statements from the body directly and
omit the final `return`.

Common headers (`stdio`, `stdlib`, `string`, `math`, `stdbool`, `stdint`,
`stddef`, `limits`, `ctype`) are pre-included. Unix builds link `-lm`; Windows
math functions come from the C runtime. GNU-style GCC/Clang drivers use
`-Wall -Wextra`; MSVC-style `cl`/`clang-cl` drivers use `/W3`. The warning is
often exactly the thing you came to check.

### Rebinding declarations and definitions

When a block-scope declaration fails with a compiler redeclaration diagnostic,
c-shell retries it inside a new nested block. If the complete program then
compiles, later inputs remain inside that block and see the shadowing binding:

```text
In [1]: int x = 1;
In [2]: x = 5;
In [3]: int x = 2;
(opened a nested scope to shadow an earlier declaration)
In [4]: x
Out[4]: 2
In [5]: %undo
undone: int x = 2;
In [5]: x
Out[5]: 5
```

`%src` shows the actual braces; no C declaration is rewritten into an
assignment. Consequently C's own scope rules still apply—for example, in
`int x = x + 1;` the initializer refers to the newly declared `x`, not the
outer one.

A rejected file-scope redefinition is handled differently: c-shell tries the
new function or type in place of each previous non-preprocessor file-scope
input, and commits only the substitution for which the real compiler accepts
the entire session:

```text
In [1]: int f(int n) { return n * 2; }
(added at file scope)
In [2]: f(3)
Out[2]: 6
In [3]: int f(int n) { return n * 3; }
(replaced previous file-scope definition)
In [4]: f(3)
Out[4]: 9
```

The old item is replaced at its original position to preserve declaration
order, and `%undo` restores it. A function-shaped input is never retried
inside `main`; this prevents GCC nested-function extensions from creating a
session that Clang or MSVC interprets differently.

![demo](demo.gif)

### Commands

`%`-prefixed, in the spirit of IPython:

```
%help      %quit      %clear     %reset     %edit [n]
%src       %type      %undo      %cc        %std
```

`%clear` erases the terminal display and returns the cursor to the top without
changing variables, retained C code or the input counter.

`%src` defaults to the user-facing program: current file-scope definitions and
retained statements inside a clean `main`, including any rebinding braces, but
without printers or protocol markers. `%src --raw` prints the complete
compiler input with `CS_PRINT`, `_Generic` and marker machinery. Either view is
formatted with `clang-format` when available; formatting is presentation only,
has a three-second deadline, and evaluation still compiles the unformatted
generated source.

`%edit` copies the most recent C input into the terminal prompt; `%edit 12`
retrieves `In[12]`. Numbered C inputs—including failed compilations and
forgotten pure queries—remain addressable until `%reset`. The command itself
does not compile, submit or consume an input number: it returns to the same
`In[n]` prompt with the selected text pre-filled and the cursor at its end.
Modify it with the normal line-editor keys and press Enter when ready; even
unchanged text is submitted only at that point as a fresh `In[n]`, while the
original numbered input remains unchanged. Ctrl-C or clearing the buffer
cancels naturally. The eventual submitted block enters process-local Up/Down
recall and follows the normal shadowing or file-scope replacement rules.
`%edit` is available only in the interactive REPL.

`%type <expression>` reports the expression type without evaluating that
expression, committing code or consuming an `In[n]` number:

```text
In [1]: const char *message = "hello";
In [2]: %type message
const char *
In [2]: %type 1 + 0.5
double
```

Aggregate categories retain an available name:

```text
In [2]: struct Point { int x, y; } point;
(added at file scope)
In [3]: %type point
Struct Point
In [3]: typedef union { int i; double d; } Value;
(added at file scope)
In [4]: Value value = { 1 };
In [5]: %type value
Union Value
```

The implementation uses C11 `_Generic`, not compiler-specific `typeof`. It
names scalar types and pointers to scalar types. Generic matching sees
compatible expression types rather than source spelling: scalar typedefs and
aliases of named tags are canonicalized, top-level qualifiers are removed by
lvalue conversion, and arrays/functions decay to pointers. Complete named
aggregate definitions visible in the session, plus simple `typedef struct {
... } Name` and `typedef union { ... } Name` forms, are added to each query
dynamically. A typedef of a named tag uses the tag's canonical name; an
anonymous aggregate typedef uses its typedef name. Truly anonymous aggregates
and other types outside the table report `<unrecognized type>`.

Tab completes `%` commands, C keywords, stdlib staples and retained session
identifiers of at least two characters. Up/Down recalls up to 1000 inputs from
the current process so multi-line blocks can be recovered, but nothing is
loaded or saved across launches. There is no `%history` or history file. The
separate current-session input archive exists only for direct `%edit n` lookup
and is cleared by `%reset`.

## How it works

**Accumulate and replay.** Every evaluation reassembles and reruns the whole
session. Block-scope declarations become ordinary locals in `main`; a
compiler-approved redeclaration starts a nested shadowing scope that encloses
later statements. Items recognized as file-scope code are emitted above
`main`, and approved redefinitions replace an older item in place. There is
no separate symbol table or declaration/initializer state store.

**`scanf` uses a session-local stdin tape.** Direct calls are routed through a
small `vscanf` wrapper. Each dynamic call in the newest input requests one
fresh line, which is retained only in memory; historical calls receive those
recorded lines and never reconnect to the terminal. Calls inside functions,
recursion and loops therefore replay in their original order. `%src` annotates
the associated statement with only a request count—the bytes remain hidden.
`%undo` and `%reset` remove the matching tape, and no tape is written to a
history file. If function replacement or changed control flow consumes a
different number of requests, evaluation stops with an explicit stdin-tape
divergence instead of silently prompting or waiting for the normal timeout.

**Classification uses a small heuristic plus trial compilation.** A lexical
heuristic routes function definitions, preprocessor directives, typedefs and
tag definitions to file scope; these inputs are never demoted into `main`,
which prevents GCC from silently accepting a nested function that Clang/MSVC
reject. The compiler arbitrates expression-versus-statement cases, but an
input whose final code token is `;` goes directly to the statement slot rather
than launching an expression compile that cannot succeed. Clearly braced
statements ending in `}` do the same; possible compound literals such as
`(int){7}` retain expression classification. After a redeclaration diagnostic,
alternate nested-scope or in-place replacement assemblies must compile as
complete programs before c-shell accepts them. File scope remains unavailable
as a general fallback.

**Value printing.** `_Generic` selects a print _function_, which is then
called — selecting a call expression instead would type-check every
unselected branch against the wrong argument type and fail to compile.
Session-visible named structs and simple anonymous struct typedefs get
recursive printers whose output uses designated-initializer form:

```text
Out[5]: {
    .name = (void *)0x7f068cbbac1b,
    .age = 30,
    .scores = {0, 7, 0},
    .next = NULL
}
```

Struct-member formatting never treats `char *` as a string: every pointer
member is `NULL` or an address, fixed arrays are traversed structurally, and a
known nested struct calls its own printer. A top-level `struct P *` likewise
prints only its address; explicit `*ptr` requests expansion. This differs
intentionally from a top-level `char *` expression, where the user explicitly
asked for the existing string dereference. Small flat structs stay on one
line; larger, nested or array-bearing structs use indented multiline output.
Multi-declarator fields, function-pointer declarators, bit-fields, flexible
arrays, C11 anonymous members and unions conservatively produce a labelled raw
byte dump instead of guessed member names.

**Bare expressions judged pure are forgotten.** A bare expression at the
prompt is usually a question (`x + 1`, `sizeof(int)`), so it is answered and
forgotten. Bare expressions that may have effects — assignments, `++`/`--`
or calls — are retained. Successfully evaluated statements/declarations
(including an expression with a trailing `;`) and file-scope items are
retained without purity analysis. `%src` shows
what will actually be replayed.

**Diagnostics are remapped and sanitized.** The compiler sees a generated file
with a prelude and all earlier statements above your input, so its line numbers
are meaningless at the prompt. Locations attributable to the newest input —
including GCC source-excerpt gutters — are rewritten to input-relative lines.
Code generation records which other lines came from retained user inputs;
source excerpts from `CS_PRINT`, `_Generic`, marker calls and wrapper code are
removed while genuine earlier-input cross-references remain. Parser fallout
that names the marker macro's internal `do` token is removed; an incomplete
expression reports `expected expression at end of input` instead of blaming
the wrapper's closing `)`.

**Compiler capabilities are cached.** Successful startup probes are cached
for seven days under the platform cache directory
(`$XDG_CACHE_HOME/c-shell`, falling back to `~/.cache/c-shell`, on Unix;
`%LOCALAPPDATA%\c-shell` on Windows). The cache key changes with compiler file
metadata, requested standard, relevant toolchain environment, or c-shell
version. Expired or malformed entries simply cause fresh probes.

## Known limitations

- **Execution is not sandboxed.** Compilers and generated programs run with
  c-shell's user permissions and working directory. Do not evaluate untrusted
  code.
- **Most external side effects still replay.** Direct standard `scanf` calls
  use the stdin tape above, but file and process changes still repeat. Calls to
  known APIs such as `fopen`, `fprintf`, `remove`, `rename` and `system`
  produce a one-time English warning before execution. Detection is lexical
  and finite: wrappers and application-defined effects can still escape it.
- **The stdin tape currently wraps `scanf`, not every input API.** `fscanf`,
  `fgets`, `getchar`, raw `read(0, ...)`, an explicit `#undef scanf`, and code
  inside a precompiled library are not captured yet. One line is supplied per
  dynamic `scanf` request; programs that require multiple interactive lines in
  a single call should put the values on one line. In piped-REPL mode stdin is
  carrying C source, so a current `scanf` receives EOF; use `-e`, `--script`,
  or an interactive terminal when the evaluated program needs stdin.
- **Rebinding is C shadowing, not assignment.** A redeclared local lives in a
  nested block. Its declarator is already in scope during its initializer, so
  `int x = x + 1;` does not read the outer `x`. File-scope replacement also
  makes retained earlier calls use the new function when the session replays.
- **The purity heuristic is conservative.** Any expression containing a
  function call is kept, even if the function happens to be pure.
- **Indeterminate values remain undefined C.** Address-only struct-member
  formatting prevents c-shell from adding a `%s` memory dereference, but it
  cannot make `struct P p; p` well-defined when members were never initialized;
  reading an indeterminate pointer or scalar is itself not portable. Initialize
  structs (for example with `{0}`) when the value matters.
- **Not every C value is printable.** The `_Generic` runtime covers the
  standard boolean/integer/real-floating types, top-level `char` strings,
  common object pointers and session-visible structs as described above.
  Header-only/anonymous aggregate types that have no reusable C spelling,
  complex and `void` values are evaluated without `Out[n]` and receive an
  explanatory note. A member whose type is not in the scalar or generated
  aggregate table is shown as `<unprintable>` rather than being coerced.
- **Program output is bounded.** When stdin is a real terminal, output from
  the newest input is streamed immediately, so a prompt before `scanf` is
  visible. If the program leaves a partial line, c-shell appends a dim `↵`
  marker and the protective newline it inserted; a real `\n` needs no marker.
  With non-terminal stdin output remains buffered for deterministic transcripts
  and receives no marker. Capture is limited to 8 MiB per stream; an overflow
  is reported and the input is not committed.
- **Timeout cleanup follows the launched process tree.** On Unix the process
  group is killed when the direct child times out, but a child that detaches
  or outlives a parent that exits successfully can escape cleanup. Windows
  `taskkill /T` has similar orphan limitations.
- **Incomplete inputs are not committed.** Besides crashes and timeouts, a
  successful early `exit(0)`, `_Exit` or top-level `return` is detected by a
  missing completion marker; committing it would terminate every later
  replay before the new input runs.
- No session save/load.
- `cl.exe` needs an MSVC build environment with `INCLUDE`/`LIB` configured;
  a Developer Command Prompt is the usual way to obtain one.
- **Windows testing is still limited.** CI covers GNU-style and MSVC compiler
  drivers, but interactive behavior has not been extensively tested on real
  Windows installations. Windows feedback and bug reports are very welcome;
  please open an issue with the terminal and compiler details.

## Development

After cloning, enable the pre-commit gate (fmt + clippy + tests — the same
checks CI runs):

```sh
git config core.hooksPath .githooks
```

The toolchain is pinned by `rust-toolchain.toml` so local clippy and CI
clippy are the same version; new lints arrive when the pin is bumped, never
by surprise on push.

Before working on the code, read [DESIGN.md](DESIGN.md): it records the
architecture decisions, the open state-model question, and several traps you
need to know about before changing things.

## License

MIT