# COBOL debugging (mcp-debugger)

## Prerequisites

- **GnuCOBOL 3.1.2+ (`cobc`)** for source launch and for COBOL-shaped variables: `apt install gnucobol3` / `brew install gnucobol` / MSYS2 `pacman -S mingw-w64-ucrt-x86_64-gnucobol`. Found via `COBC_PATH`, then PATH, then the usual install directories (`C:\msys64\mingw64\bin`, `/opt/homebrew/bin`, `/usr/bin`, …). `mcp-debugger doctor cobol` shows what was found.
- **CodeLLDB is vendored** (shared with Rust and C/C++; `CODELLDB_PATH` overrides). No system LLDB.
- **Windows**: MSYS2's cobc needs `COB_CONFIG_DIR` and its `bin` on PATH when run outside an MSYS2 shell; the adapter sets both when it finds cobc, so you only care if a compile fails with `configuration error: …default.conf`.
- The adapter compiles with `cobc -x -g -fdump=ALL --save-temps -t <lst> -ftsymbols -A "-O0 -gdwarf-4"` into `.debug-mcp/cobol/<name>/<buildKey>/` beside the source (a translate-only manifest for a prebuilt binary or an attach goes to `<name>-manifest/`) (`-gdwarf-4`: MinGW's default DWARF-5 line tables are unreadable by LLDB, breakpoints would never bind). A prebuilt executable must have been built with `cobc -g` the same way.
- **Docker**: the image installs `gnucobol3`, so `.cob` launch compiles in-container; prebuilt binaries must be Linux-built for the image's architecture.

## Launch quickstart

```json
create_debug_session  {"language": "cobol", "name": "payroll"}
set_breakpoint        {"sessionId": "<id>", "file": "C:/proj/src/PAYROLL.cbl", "statement": "COMPUTE WS-NET = WS-GROSS - WS-TAX"}
start_debugging       {"sessionId": "<id>", "scriptPath": "C:/proj/src/PAYROLL.cbl",
                       "dapLaunchArgs": {"dialect": "ibm", "format": "fixed", "copybookDirs": ["C:/proj/copybook"],
                                         "runtimeChecks": true, "stdinFile": "C:/proj/test/sysin.txt"}}
get_stack_trace       {"sessionId": "<id>"}
get_local_variables   {"sessionId": "<id>"}
evaluate_expression   {"sessionId": "<id>", "expression": "WS-RATE OF WS-EMP-REC"}
step_over             {"sessionId": "<id>"}
continue_execution    {"sessionId": "<id>"}
get_output            {"sessionId": "<id>"}
close_debug_session   {"sessionId": "<id>"}
```

- `scriptPath` is a **`.cob`/`.cbl`/`.cobol` source** (auto-compiled; rebuilt only when cobc version, flags, sources or copybooks change; `"forceRebuild": true` to force — every build gets its own directory, nothing is relinked under a paused session) or a **prebuilt executable** — then add `"sources": ["<abs .cob>", …]` so the symbol manifest is regenerated by a translate-only `cobc -C` with the same `dialect`/`copybookDirs`/`runtimeChecks` the binary was built with; without a manifest variables show only the engine's C view (`b_8`, not `WS-TOTAL`). cobc is needed only for a source launch or that regeneration.
- `dapLaunchArgs` keys (equally accepted in `adapterLaunchConfig`, which wins on a conflict): `dialect` (`-std=`: `ibm`, `mf`, `cobol85`, …), `format` (`fixed`/`free`), `copybookDirs` (`-I`; the program's own directory is always searched, so this is for copybook libraries elsewhere), `sources` (statically linked programs), `modules` (`cobc -m` dynamic modules named after their PROGRAM-ID, put on `COB_LIBRARY_PATH`), `runner` (`"cobcrun"`: the program is built as a module and run under `cobcrun <PROGRAM-ID>`; a prebuilt `.so`/`.dll` module is run by name), `cobcFlags` (verbatim), `runtimeChecks` (`--debug`: subscript/ODO/ref-mod/numeric checks — required for the runtime-error stop), `stdinFile` (SYSIN; the only way ACCEPT gets input), `engineScopes` (also show CodeLLDB's Local/Static/Global), `manifestDirs`. Program args go in top-level `args`; ASSIGN'd files via `env` (`DD_<name>`, `COB_FILE_PATH`). CodeLLDB keys (`initCommands`, `sourceMap`, …) pass through.
- Breakpoints go on PROCEDURE DIVISION lines of `.cob` **or `.cpy`** files. A breakpoint in a `modules` entry stays unverified until the module loads — normal.

## Attach (by PID)

```json
attach_to_process {"sessionId": "<id>", "processId": 4242, "adapterConfig": {"sources": ["C:/proj/src/payroll.cob"], "dialect": "ibm"}}
attach_to_process {"sessionId": "<id>", "processId": 4242, "adapterConfig": {"manifestDirs": ["C:/proj/src/.debug-mcp/cobol/PAYROLL/<buildKey>"]}}
```

- Numeric `processId` only (no host/port). Target held **paused** after attach (`stopOnEntry: false` to resume). `detach_from_process` leaves it running.
- `sources` (+ the `dialect`/`format`/`copybookDirs`/`runtimeChecks` the binary was built with) regenerates the symbol manifest by a translate-only `cobc -C`, beside `program` if you name the binary; `manifestDirs` = directories holding ready `*.cobol-symbols.json` (an earlier source launch's artifact directory) and needs no cobc. With neither, the session shows the engine's C view.
- Attach usually lands inside libcob / `C$SLEEP` (no COBOL source at the top): scopes, locals and `evaluate_expression` serve the nearest COBOL program up the stack, named in the scope (`WORKING-STORAGE of PAYROLL (0000-MAIN, 3 frames up)`).
- Linux: `kernel.yama.ptrace_scope=1` limits attach to child processes (`sudo sysctl kernel.yama.ptrace_scope=0`).

## Quirks

- **Scopes are `WORKING-STORAGE` / `LOCAL-STORAGE` / `LINKAGE`** (not Locals/Globals); `get_local_variables` is their union. Paused outside COBOL (libcob, `C$SLEEP`, generated C) they are the nearest COBOL program's up the stack, named `WORKING-STORAGE of PAYROLL (0000-MAIN, 3 frames up)` — inspect right there; only a stack with no COBOL program at all is empty with a note. FILE SECTION is not a scope yet.
- **Values are COBOL-decoded**: `-123.45` for DISPLAY/COMP/COMP-3/COMP-5 numerics (COMP is big-endian, COMP-5 native), quoted `"ALICE   "` text, groups as a byte preview that expands to subordinates, `OCCURS` (incl. `DEPENDING ON`) as one child per occurrence, `REDEFINES` as siblings, **88-levels as `true`/`false`**, bad packed data as `<invalid packed: 0x…>`. `type` is COBOL vocabulary (`PIC S9(5)V99 COMP-3`).
- **`evaluate_expression` takes data-names**: `WS-TOTAL`, `WS-ID OF WS-GROUP` (or `IN`), `WS-AMOUNT(3)`, `WS-NAME(1:5)`, `LENGTH OF X`, `ADDRESS OF X`; suffix `/hex`, `/raw`, `/addr`, `/len` for the bytes/address/length; `/nat <expr>` evaluates a raw CodeLLDB C expression instead (`/nat (unsigned long long)(b_24 + 24)`).
- **Stepping is one COBOL statement** (the shim loops the engine over the generated C until a statement the manifest knows — copybook statements included — or a paragraph header; the first step from a paragraph header reaches its first statement). `step_into` on CALL enters the callee (its paragraph header or first statement); `step_out` from a CALLed program returns to the caller's next statement, and from the outermost program it stops in the generated C `main` (under `runner: "cobcrun"` it runs the job to completion instead: the loader has no frame to stop in). **`step_over` on an out-of-line `PERFORM` runs the whole performed range** (`TIMES`/`UNTIL`/`VARYING` loops included, one return cycle per iteration — `pause_execution` interrupts a long one) and stops on the statement the program executes next; `step_into` on a PERFORM enters the paragraph; `step_out` inside a performed paragraph returns to the next statement at a shallower PERFORM depth. A `GO TO` out of the range escapes the step (the program runs to the next breakpoint — set one after the PERFORM).
- **Runtime errors pause before the abort**: the launch default `breakOnExceptions: "uncaught"` (= `"all"`, one filter `cobol_runtime_error`) is a function breakpoint on libcob's `cob_runtime_error` — subscript out of bounds, non-numeric data, ref-mod out of range: the S0C7/SSRANGE analogues. **Needs `runtimeChecks: true`**, otherwise no check exists to trip. The visible top frame is the offending statement; `continue_execution` lets libcob print its message and exit 1. `STOP RUN` never triggers it.
- **Output**: `DISPLAY` lands in `get_output` (POSIX via CodeLLDB output events; Windows via adapter stdio).
- **Function breakpoints are paragraph, section or PROGRAM-ID names**: `set_breakpoint {function: "1000-INIT"}` (qualify with `OF PROG` / `PROG:` when several programs have the name) binds a source breakpoint on the range's first statement; a hit reports it in `hitBreakpointIds`. **Logpoints** interpolate COBOL data references: `logMessage: "total={WS-TOTAL} idx={WS-IDX}"` (or `{/nat expr}`), logged to `get_output` without pausing.
- **The stack trace lists the active PERFORMs** as frames under the program (`HELLO: 0000-MAIN (PERFORM 1000-INIT)` at the PERFORM line); scopes and expressions on them read the program's storage.
- **Not yet**: writing variables. Same engine as C/C++, so its quirks apply (possible launch-time system stop → one `continue_execution`; three harmless `DW_AT_ranges … range extraction failed` LLDB errors per launch on Windows).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Compile fails `configuration error: …\default.conf: No such file` | MSYS2 cobc without `COB_CONFIG_DIR` | Set `COB_CONFIG_DIR=<prefix>/share/gnucobol/config` (automatic when cobc is found via `COBC_PATH`/PATH/known dirs) |
| Breakpoint `verified: false`, "Resolved locations: 0", program runs through | Binary built without `-g` / without `-gdwarf-4` on MinGW | Let the adapter compile, or `cobc -g -A "-O0 -gdwarf-4"` |
| `libcob-4.dll not found`, program dies at start (Windows) | GnuCOBOL `bin` not on PATH | Automatic when cobc is found; else add `<prefix>/bin` to PATH or `env` |
| Variables are `b_8`-style byte arrays, `get_local_variables` empty | No symbol manifest (prebuilt without `sources`/`manifestDirs`, or cobc missing) | Pass `sources` (cobc installed) or `manifestDirs` |
| "GnuCOBOL (cobc) not found" | Not installed / off PATH | Install, or `COBC_PATH`; prebuilt-only hosts: `MCP_COBOL_ALLOW_PREBUILT=true` |
| Runtime error never pauses | `runtimeChecks` unset | `"runtimeChecks": true` |
| Module breakpoint never verifies | Module not loaded / not found | Wait for the CALL; check `modules` / `COB_LIBRARY_PATH` |
| `cobol-shim.js not found` | Adapter package not built | `pnpm --filter @debugmcp/adapter-cobol run build` |
