# cpp-style directives in a netlist

## The problem

A stimulus file for one chip holds every test you ever run against it — power-up,
power-down, SPI load, trim, test mode, parallel mode. Only one of them applies to
the run in front of you. Eldo decks solve this with the C preprocessor:

```
#ifdef testmode
  ... configure the chip in test mode and load the fuse register ...
#endif
```

and the test is selected from the command line. ngspice had nothing equivalent.
A line starting with `#` was silently turned into a comment, so both branches
survived and the deck was wrong in a way that produced no error.

The workaround is to run the deck through `cpp` first. It does not survive
contact with real netlists:

- `cpp` owns file inclusion, so it only understands `#include`. Keeping `.include`
  means rewriting the deck, which forfeits ngspice's search paths, `.lib` section
  syntax, and file/line provenance in error messages. (This is why Spectre makes
  you spell it `#include` once any directive appears in the file — the tail
  wagging the dog.)
- `#ifdef 2x` is not a C identifier. `cpp` rejects it; a netlist is happy with it.
- Netlists are full of apostrophes — `hmax='1/(tp*20)'` — which ISO `cpp` reads as
  unterminated character constants.
- `#define powerdown` in C deletes every later occurrence of the token `powerdown`. In a
  netlist `powerdown` is a *node name*. The C rule silently destroys the circuit.

## What ngspice-jit does

The directives are handled inside `inp_read()`, on each line as it comes off
`readline()` and before anything else touches it — before the `.control` counter,
before `.include` recursion, before case folding. `inp_read()` is already the
recursive function that expands `.include`, so:

- **`.include` keeps working exactly as it always did.** There is no second
  spelling to learn and nothing to rewrite. `#include` is accepted as a plain
  synonym for those who want it, and is handed straight to the `.include` code.
- **A `.include` inside a false branch is never opened**, because the filter sits
  above the `.include` handler in the same loop.
- ngspice's own path resolution, line numbering and error messages are untouched.

### Directives

```
#define name              a flag
#define name value        a macro with a value
#undef name
#ifdef name               #ifndef name
#if expr                  #elif expr        #else       #endif
#error message            #warning message
#include "file"           synonym for .include "file"
```

`#if` / `#elif` expressions take `defined(name)`, `!`, `&&`, `||`, parentheses,
integer literals, `== != < > <= >=`, and bare macro names (a valued macro
contributes its value read as an integer; anything else is 0 or 1 by existence).

### Setting flags from outside the deck

`-D` already existed and needed no change:

```
ngspice -b -D testmode -D uic chip.cir
```

`#ifdef name` is true if `name` was `#define`d **or** if a variable of that name
exists, so `set testmode` in `.spiceinit` or `spinit` selects branches too. One
switch drives both the read-time `#ifdef` and a run-time `if $?testmode` inside
`.control`.

`set cpp_verbose` (in `.spiceinit`, or `-D cpp_verbose`) traces every directive
and the branch it selected:

```
cpp: stimuli.inc:23: #ifdef remote -> skipped
cpp: stimuli.inc:25: #else -> taken
cpp: stimuli.inc:32: #ifdef fast -> taken
```

Setting it inside `.control` is too late — see below.

## Deliberate differences from C

| | ngspice-jit | C |
|---|---|---|
| macro names | case-insensitive, like the rest of a netlist | case-sensitive |
| legal names | netlist identifiers: `2x`, `sig<2>`, `sig<7>` | C identifiers only |
| `#define name` (empty) | a **flag**. Never substituted. | expands to nothing, deleting the token |
| `#define name value` | substitutes whole tokens, outside quoted strings | same, plus function-like macros |
| `# ifdef` (space) | **not** a directive, stays a comment | a directive |

The empty-macro rule is the important one. `#define powerdown` must not touch the
node `powerdown`, and node names and flag names come from the same vocabulary in
practice, so an empty macro is treated as a flag and never substitutes.

The no-space rule protects existing decks. A line beginning with `#` has always
been a comment in ngspice, and comments like `# if you change this ...` exist in
the wild; requiring the keyword to follow `#` immediately leaves them comments.

## Interaction with `.control ... .endc`

Control-block lines go through the same reader, so directives work inside
`.control` and choose **which commands exist in the block**. That is more
powerful than the runtime `if`, but it happens at a different time, and the two
must not be confused:

| | when it is decided | driven by |
|---|---|---|
| `#ifdef` | when the deck is read | `-D`, `set` in `.spiceinit`, `#define` |
| `.if(expr)` | after the deck is assembled and parameters are evaluated | `.param` expressions |
| `if / else / end` in `.control` | when the commands run | any variable or vector, live |

Consequences worth knowing:

- A `set` inside `.control` cannot influence `#ifdef`: the whole deck has already
  been read by the time any command runs. Put such flags on the command line or in
  `.spiceinit`.
- `reset` re-elaborates the card list that was already preprocessed. Branch
  selection is fixed for the life of the loaded deck.
- Do not straddle a `.control`/`.endc` pair across a conditional — putting
  `.control` in one branch and `.endc` in another unbalances ngspice's control
  nesting. An unterminated `#if` is reported with the file and line where it was
  opened; an unbalanced `.control` is not.
- Command files (`spinit`, `.spiceinit`, `source`d `.cmd` files) are read through
  the same path, so directives work there as well.

## Where it does *not* apply cleanly

`.lib` files. Libraries are read lazily by `read_a_lib()` during
`expand_section_references()`, which runs at the end of the top-level read, and
lib decks are cached by real path. So the macro state a `.lib` file sees is
"end of deck", not "point of use", and a second `.lib` of the same file reuses the
first expansion. Use `#ifdef` in the main deck and in `.include`d files, where it
is exact.

## Lifetime

Macros created by `#define` in a netlist are cleared at the start of each
top-level netlist read, so `source`ing another deck starts clean. Flags from `-D`
and `set` are variables and live for the session.

## Code and tests

- `src/frontend/inppp.c`, `src/frontend/inppp.h` — the preprocessor.
- `src/frontend/inpcom.c` — the hook in `inp_read()`, the per-file balance check,
  and the per-deck reset in `inp_readall()`.
- `tests/regression/parser/cpp-1.cir` — the regression test. Its load-bearing
  assertion is that the *unselected* branch names an include file that does not
  exist: with the preprocessor the run completes, without it ngspice aborts trying
  to open the file and none of the markers are printed.
