Skip to content
| Marketplace
Sign in
Visual Studio Code>Debuggers>Fortran Debug (ifx / gfortran)New to Visual Studio Code? Get it now.
Fortran Debug (ifx / gfortran)

Fortran Debug (ifx / gfortran)

PbsandtDev

|
255 installs
| (0) | Free
VS Code debugger adapter for Fortran with Fortran-aware evaluate and breakpoint fallbacks. Supports Intel ifx (Windows) and GNU Fortran + GDB (Windows/Linux); GNU Fortran + LLDB (macOS) is an experimental feasibility target.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Fortran Debug (ifx / gfortran)

Fortran Debug is a VS Code debugger extension that wraps the Microsoft C/C++ debugger backends with a Fortran-aware proxy.

It adds a custom fortran-debug debug type and focuses on the cases where plain cppvsdbg or cppdbg are weak for Fortran:

  • module-variable lookup and evaluate fallback for symbols and expressions
  • better watch / hover / debug-console evaluation
  • synthetic array and array-section browsing
  • derived-type field recovery
  • conditional breakpoint rewriting for common Fortran syntax

On Windows, the extension can use either cppvsdbg (native Intel ifx debugging) or cppdbg with GDB (a GNU/MinGW build). On Linux it routes through cppdbg with GDB. On macOS, cppdbg with LLDB is an experimental feasibility target, not a tested or supported configuration -- see Current Scope And Limits below before relying on it.

Which recovery mechanism actually runs depends on the resolved compiler and debugger for the session (debug_capabilities reports it), not just the platform: Intel's own symbol-mangling and array/derived-type recovery only run for an Intel ifx profile. A GNU Fortran + GDB session instead relies on GDB's own native Fortran evaluator for those, with this extension's generic scope-walk and inquiry fallbacks layered on top for both.

What The Extension Actually Adds

  • A fortran-debug debug type contributed to VS Code.
  • A proxy debug adapter that sits in front of the cpptools backend.
  • Evaluate fallback for Fortran symbols and expressions:
    • Intel ifx: symbol-map based mangling through fortran_symbols.json, plus C-style descriptor/pointer-cast recovery for arrays and derived types.
    • GNU Fortran + GDB: GDB's own native Fortran evaluator resolves module variables, allocatable arrays, non-default bounds, and derived types directly; this extension does not re-mangle or cast for it.
    • Both: scope-walk fallback for module and USE-associated variables, and inquiry fallback for allocated, associated, size, shape, lbound, and ubound.
  • Synthetic array views for allocatable arrays and array sections:
    • multidimensional arrays
    • strided sections
    • empty sections
    • open-bound sections when bounds can be inferred
  • Synthetic derived-type object expansion using source-parsed field lists.
  • Breakpoint-condition rewriting for common Fortran syntax:
    • .and., .or., .eq., /=, and similar operators
    • single-quoted string literals
    • Fortran array subscripts
    • A GDB session receives the original Fortran condition unchanged -- GDB already understands it natively, so nothing is rewritten for it.
  • Automatic generation of fortran_symbols.json at launch, when the helper script and its prerequisites are available.
  • debug_crash_report explains a runtime crash that never reached the debugger for either compiler: Intel ifx by resolving its traceback against the PDB, GNU Fortran from libgfortran's own file/line diagnostic text (its backtrace addresses are not resolved yet).

Current Scope And Limits

  • launch is the contributed workflow today. attach is not exposed in the current debugger contribution yet.
  • The extension depends on the Microsoft C/C++ extension (ms-vscode.cpptools) because it delegates actual debugging to cpptools.
  • The most advanced Fortran recovery features are source-assisted. They work best when your project layout looks like this:
    • symbols under build/debug/fortran_symbols.json
    • Fortran sources under src/
  • Native Windows PDB/DbgEng inspection is not implemented yet. The extension still relies on cpptools plus symbol/source fallbacks.
  • Compiler and debugger support, stated plainly:
    • Intel ifx + cppvsdbg (Windows): the original, most-tested path.
    • GNU Fortran + GDB (Windows): scalars, arguments, module variables, caller-frame values, arrays (including non-default lower bounds), derived types, ambiguous module-name detection, and common runtime-error diagnostics are validated against a real GDB session. The same profile is available on Linux, with cross-platform unit and lint coverage, but the complete live Linux debugger matrix is not yet recorded.
    • GNU Fortran + LLDB (macOS): an unverified feasibility target. Nothing here has been confirmed to work on macOS; treat it as experimental.

Requirements

  • VS Code 1.101 or newer
  • Microsoft C/C++ extension (ms-vscode.cpptools)
  • A debug build of your Fortran program with symbols
    • Windows ifx: PDB symbols
    • Linux/macOS: DWARF symbols for GDB/LLDB
  • Optional, for automatic symbol-map generation:
    • Python 3
    • llvm-nm or nm on PATH

Install

If you are using a packaged build of the extension:

  1. Open VS Code.
  2. Run Extensions: Install from VSIX....
  3. Select the .vsix file for this extension.
  4. Make sure ms-vscode.cpptools is also installed.

Quick Start

  1. Build your Fortran project in a debug configuration.
  2. Open the workspace in VS Code.
  3. Add a launch configuration with "type": "fortran-debug".
  4. Start debugging as usual from the Run and Debug view.

No shared CMakeLists.txt changes are required for this workflow.

Commands

The extension provides these commands, all under the Fortran Debug category in the command palette:

Command Purpose
Create Fortran Launch Config Add or update a launch.json entry for Intel ifx (cppvsdbg), GNU + GDB, or GNU + LLDB
Create Windows Launch Config Compatibility alias: only ever writes the Windows/Intel entry
Open Proxy Log Open the proxy adapter's diagnostics log
Refresh Symbol Map Regenerate fortran_symbols.json without restarting
Break Where A Variable Is Assigned Place conditional breakpoints at every line that writes a variable
Show Tamandua Facts Status Report which facts file is in use and what it can answer
Write MCP Server Config Point .vscode/mcp.json at the extension's MCP server

Create Fortran Launch Config asks which compiler and debugger the configuration targets, then adds or updates the matching entry in .vscode/launch.json for the active workspace folder -- Intel ifx via cppvsdbg (Windows), GNU Fortran via GDB (Windows or Linux), Intel ifx via GDB (Linux), or GNU Fortran via LLDB (a measured feasibility target on macOS, not a promise of full parity). All four use the generic ${command:cmake.launchTargetPath} program pattern. This is the one to reach for regardless of platform or compiler; Create Windows Launch Config remains only as a compatibility alias that always writes the original Windows/Intel entry.

Both write through the VS Code configuration API, which rewrites the configurations block from parsed JSON, so comments in launch.json are not preserved by that write. Create Fortran Launch Config checks for comments first and asks before overwriting; the older command does not.

Open Proxy Log opens the proxy adapter's log file. The proxy runs as a separate process and cannot write to the extension's output channel, so its diagnostics — evaluate fallbacks, allocate-index builds, scope-walk stats — only exist there. That log is the first place to look when a variable does not resolve.

Refresh Symbol Map regenerates build/debug/fortran_symbols.json for the active workspace folder without restarting the debug session, for when the map has gone stale after a rebuild. It requires Python 3 on PATH.

Breakpoints From A Variable Name

Setting a useful Fortran breakpoint means knowing three things the debugger cannot tell you: which lines assign the variable, what loop encloses each of them, and which index variable pins one iteration. Those are static facts, and Tamandua indexes them.

Point the extension at a Tamandua facts file and run Fortran Debug: Break Where A Variable Is Assigned. Give it aqu_d%rchrg, and it finds every write, offers the index variables in scope at each, and places a breakpoint conditioned on the iteration you name.

The facts file is looked for in this order:

  1. fortranIfx.factsFile, if set
  2. swatplus-facts.json in the workspace folder
  3. build/swatplus-facts.json
  4. build/debug/swatplus-facts.json
  5. .tamandua/swatplus-facts.json

Build one with swatplus-build from a Tamandua checkout. Without one, this command reports that it has no facts and everything else in the extension works as before -- the feature is opt-in and nothing depends on it.

What it will not do is guess. Where Tamandua could not resolve a file's loop nesting it places the breakpoint without a condition and says so, because a condition pinned to the wrong index variable costs a whole compile-and-run cycle to discover. Where a bare module-variable name is declared in more than one module, it narrows the candidates by what the file imports -- a breakpoint carries no stack frame, so there is no running scope to consult -- and reports the ambiguity rather than picking one.

Trying It Without A Real Index

test_program/ ships a hand-written swatplus-facts.json describing its own src/main.f90, so the feature can be exercised with no SWAT+ checkout and no Tamandua install. Open test_program/ as the workspace folder, run Fortran Debug: Break Where A Variable Is Assigned, enter total_ke, and answer 2 when it asks for an iteration. It places two breakpoints:

main.f90:108 in run_demo  when step == 2
main.f90:111 in run_demo  when k == 2

Two different conditions for one variable, because only the inner loop encloses the second write. That is the behaviour worth checking -- a single condition applied to both lines would be wrong at one of them.

Other things to try in that fixture: grid (nested loops in fill_grid), p%position%x (a routine with no loop of its own, so the index comes from the caller), and pi (a parameter, which nothing assigns).

Letting An Assistant Set Them

A VS Code breakpoint is not a file. Breakpoints live in the editor's workspace storage, so an AI assistant that edits files and runs shell commands has nothing to write, and only an extension can call vscode.debug.addBreakpoints.

So the extension runs a small MCP server on loopback and registers it with the editor, which makes these tools available to agent mode:

Tool What it does
break_at Place a breakpoint at a file and line, optionally conditional
set_fortran_breakpoint Place conditional breakpoints for a variable
plan_fortran_breakpoint Report where to break and on what, changing nothing
clear_fortran_breakpoints Remove every breakpoint in the workspace
start_fortran_debug Run until a breakpoint hits, then report the stop
debug_variables Every variable in scope at the stop, or in a caller's frame
debug_evaluate Read a Fortran expression, with a derived type's components
debug_continue Run to the next hit
debug_step Step over, in, or out
debug_output What the program has printed, most recent last
debug_crash_report Name the failing subroutine when a crash never trapped
stop_fortran_debug Terminate the session

Recorded/automated investigations should use the ownership-aware contract instead of the global tools above:

Tool Ownership guarantee
debug_session_state Reports the active session ID, resolved workspace/program/cwd, and stop sequence
place_owned_fortran_breakpoint Adds one breakpoint without replacing another and returns its handle
remove_owned_fortran_breakpoint Removes exactly the returned handle
start_owned_fortran_debug Verifies the named configuration resolves to the caller's expected run
debug_owned_evaluate Requires session and stop identity; assignments have no override
debug_owned_continue / debug_owned_step Requires the expected session ID and returns the next stop identity
stop_owned_fortran_debug Terminates only the expected session ID

These tools use contract vsc-ifx-debug-owned-session/1. A timeout means no stop was observed; it does not mean the program stopped. The legacy tools stay available for interactive use, but an automated client must not fall back to clear_fortran_breakpoints, mutation-enabled evaluation, or unscoped session controls when this contract is unavailable.

The six debugging tools need 0.5.0 or newer, debug_continue needs 0.6.0 to survive more than one call, debug_evaluate needs 0.7.0 to report a derived type's components rather than {...}, break_at arrived in 0.8.0, debug_output needs 0.9.1 -- 0.9.0 captured cppvsdbg's own telemetry alongside the program's real output, which crowded it out of the buffer -- debug_crash_report arrived in 0.10.0, and debug_evaluate needs 0.11.1 to refuse an assignment (x = 2) rather than silently letting the Watch evaluation it forwards write into the running program. log_message and hit_condition on break_at and set_fortran_breakpoint need 0.12.0. An agent that lists only the three breakpoint tools is running an older build: check the version in the Extensions panel rather than the tool names, since 0.4.0 shipped twice with different tool sets.

A crash that never reached the debugger

A severe Fortran runtime error arrives in one of two ways, and they need different things.

When the debug build traps it, the debugger stops and cppvsdbg hands over a real PDB-resolved stack. The top frame or two sit inside libifcoremdd.dll, which ships no PDB, but the frame below that is the user subroutine and line. That case needs nothing.

When it does not trap -- a release build, or any configuration where the runtime just exits -- there is no stop and no stack. All that survives is what forrtl printed on the way out:

forrtl: severe (408): Subscript [#1](https://github.com/tugraskan/vsc_ifx_debug/issues/1) of the array OB has value 0 which is less than the lower bound of 1
Image              PC                Routine            Line        Source
swatplus-62.0.0-9  00007FF637696D74  Unknown               Unknown  Unknown

Every routine says Unknown, because Intel's own unwinder could not read the very PDB cppvsdbg reads successfully. The names are gone; the addresses are not. debug_crash_report works from those.

The arithmetic it undoes is why those addresses look useless. A printed program counter is a loaded address, and Windows relocates the image, so it matches nothing in the PDB and differs on every run:

rva    = pc - load base
lookup = preferred base + rva

The load base comes from the adapter's module events, recorded while the process is alive -- by the time the traceback prints the process is going away and nothing can be asked of it, so it is collected up front for a crash that may never come. The preferred base is read out of the PE header rather than assumed, because a link that set /BASE would make every resolved line wrong by the difference. Each frame prints both addresses, so a wrong base shows up rather than hiding behind a plausible source line.

Resolution needs a tool that reads PDBs. Intel ships llvm-symbolizer with oneAPI, so it is usually already on the machine that compiled the program; set fortranIfx.symbolizerPath if it lives elsewhere. Without one, the report still gives the condition, the array, the index and the bound, and names every path it searched.

The last section of a report -- what the static index knows about the named array -- is the only part that is not evidence, and says so. A subscript violation happens at a read, and the index records assignments, so those sites are worth looking at and are not the failing line.

Reading what the program said, not just where it stopped

Not every failure is a stop. A SWAT+ input error -- a malformed line in file.cio, a missing value -- is usually a clean Fortran stop after a printed message, not a trapped exception. There is no frame to inspect and no breakpoint to hit, because the failure happens before any breakpoint in your own code is ever reached. The printed message is the whole diagnostic, and until 0.9.0 nothing here could retrieve it: start_fortran_debug would only report "the program exited before stopping" and point at the Debug Console, which a caller without eyes on the editor cannot read.

debug_output reads what the program printed, and the termination error itself now folds in the last part of it directly -- the common case needs no second call. It works during a run, at a stop, or after the session has already exited; the buffer is cleared only when a new session starts, so a crash is still readable after the fact.

An exception that is trapped (an access violation, a runtime bounds check) still produces a stop, and start_fortran_debug and debug_continue now report its detail -- the debugger's own description of what happened, since an exception has no breakpoint condition to echo in its place.

Conditions

set_fortran_breakpoint takes either index_value or condition:

  • index_value pins the innermost counted loop at the breakpoint. Use it when the iteration you want is the loop right there in the routine.
  • condition is Fortran, used instead. Use it when the index that selects the object is set by the caller, which no loop in the routine carries.

hru_control is the case that needs the second form. It runs once per HRU, and j = ihru arrives from command, so the innermost loop there is over soil layers -- index_value: 3 means layer 3, while HRU 3 is condition: "j == 3". The placement report echoes the condition, so which one you got is visible rather than assumed.

A loop index itself is never a breakpoint target: a DO index has no assignment statement, so j1 has no site of its own. The planner says so and names the loop it drives, rather than reporting the variable as unknown.

The last six are what a print statement cannot give you. With prints, the variables to log have to be chosen before rebuilding, and a wrong guess costs another full SWAT+ build. At a breakpoint everything in scope is already there, so an assistant can ask twenty follow-up questions for free, and step out to the caller to find where an index came from.

debug_evaluate deliberately goes through the session rather than around it: the request passes the proxy, so it inherits the Fortran fallbacks the proxy already implements -- mangled module symbols, allocatable arrays, derived-type expansion -- instead of reimplementing them worse.

The tool reads; it does not write. The DAP evaluate request it sends runs in the Watch context, and a Watch expression is free to be an assignment -- x = 2 sets x and reports success exactly like a genuine read would, with nothing in the response marking it as a write. So an expression containing an assignment token (x = 2, soil(1)%phys(1)%st = 0, or a nested foo(x=1)) is refused. Comparisons such as ==, /=, <=, and >= remain ordinary reads. Ambiguous keyword-argument or implied-do syntax must use the explicit override because a C-like debugger can interpret its nested = as mutation. Mutation is available, but only on purpose: pass allow_mutation: true to run an assignment anyway, and the result comes back with a warning that program state may have changed.

Stopping on one object

break_at is the plain operation and usually the right one. Stopping on HRU 3 is not the same as reading soil(3) from a stop on HRU 1: the editor's Locals and Watch panes show the frame the program is actually parked in, a routine's per-object locals do not exist yet for an object it has not reached, and a value read mid-run is whatever it was before that object's update.

break_at  file=hru_control.f90  line=102  condition="j == 3"

hru_control runs once per HRU with j = ihru arriving from command, so the index that selects the object is not carried by any loop in the routine -- which is why index_value cannot express this and a condition can. The innermost loop there is over soil layers, so index_value: 3 would mean layer 3.

It reports which procedure the line is in and which loop indexes are live there, so the position can be confirmed before a run. It does not move the breakpoint: if a line has no code of its own -- a declaration, a comment, a loop header the compiler folded -- the breakpoint stays hollow when the run starts, and the next executable line is the answer.

set_fortran_breakpoint remains the way in when the line is the unknown: give it a variable and it finds every site that assigns it.

Tracing a value without stopping the run

A SWAT+ run can take minutes to reach a conditional breakpoint, and every sample means stopping, reading, and continuing -- a round trip per value. log_message on break_at or set_fortran_breakpoint places a logpoint instead: the run never stops, and VS Code evaluates the message and prints it to the Debug Console each time the line is hit.

break_at  file=hru_control.f90  line=102  log_message="hru 3 rchrg={aqu_d(iaq)%rchrg} day {time%day}"

Text outside {...} is printed as-is; a Fortran expression inside {...} gets the same rewriting a condition gets -- .and., array subscripts, mangled module symbols -- so it reads the way it would in the Watch pane. Combine with condition to trace only some iterations while still never stopping, or with hit_condition (">5", "%10", ...) to sample every Nth hit instead of every one. On set_fortran_breakpoint, log_message and hit_condition apply to every site the plan finds, so one call traces a variable across every routine that assigns it.

Both cpptools backends this extension drives (cppvsdbg on Windows, cppdbg/OpenDebugAD7 elsewhere) document logpoint support, but the tool checks the live session's own initialize response rather than assuming it: a backend that has already answered no is refused with a clear reason instead of silently placing a breakpoint that stops on every hit.

Seeing the rest of a type once stopped

At a stop, debug_evaluate reports a derived type's components rather than {...}:

debug_evaluate  soil(3)              -> the profile, component by component
debug_evaluate  soil(3)%phys(2)  depth=2

depth defaults to 1 and is capped at 3. A profile nests components inside arrays of layers, so an uncapped read would bury a whole context in one call; children per value and total nodes are bounded too. Every cut is marked with a + or a count, never silent -- an omitted interior that says nothing reads as a leaf, and the next question then assumes the data is not there.

Because module-level arrays cover every object, a neighbour is also readable from where you are -- soil(4) while parked on HRU 3, no resume. Useful for a comparison; not a substitute for stopping on the object you care about.

Prints remain better for breadth over time: 365 days of a value in a file beats one frame. The two are complementary, and Tamandua improves both, since most of what makes print debugging slow is not knowing which line to print at.

The server binds 127.0.0.1 only. Its tools change editor state, so nothing off-box may reach it. Set fortranIfx.mcpServer.enabled to false to switch it off.

How a client reaches it depends on whose MCP registry it reads.

VS Code's own agent mode is registered with automatically, through vscode.lm.registerMcpServerDefinitionProvider. Nothing to configure, and the per-session port is handled for you. Fortran Debug: Write MCP Server Config additionally records the address in .vscode/mcp.json.

Claude Code, and any other external client, does not read VS Code's registry -- it keeps its own MCP configuration -- so it has to be pointed at the URL, and it stores that URL rather than re-reading it. Pin the port first, or the address will be wrong after the next reload:

// .vscode/settings.json
{ "fortranIfx.mcpServer.port": 39271 }

Then add the server to that client. For Claude Code:

claude mcp add --transport http fortran-ifx http://127.0.0.1:39271

Reload the window after pinning the port, and check the "Fortran Debug" output channel for mcp server listening on http://127.0.0.1:39271. If the port is already taken the extension says so rather than failing quietly.

Launch Configuration

Generate any of these with Fortran Debug: Create Fortran Launch Config (recommended), or write them by hand as shown below.

Windows, Intel ifx (cppvsdbg)

Use cppvsdbg implicitly by omitting MIMode:

{
  "name": "Ugs Debug (Windows IFX)",
  "type": "fortran-debug",
  "request": "launch",
  "program": "${command:cmake.launchTargetPath}",
  "args": [],
  "cwd": "${workspaceFolder}",
  "stopAtEntry": false,
  "environment": [],
  "externalConsole": false
}

Windows Or Linux, GNU Fortran + GDB

The most-tested non-Intel path (see Current Scope And Limits above). Set fortranCompiler explicitly: MIMode: "gdb" alone does not imply GNU (an ifx build debugged through GDB is representable too), so this is what tells the extension's evaluate/breakpoint/crash-report recovery to gate itself to what has actually been validated for GNU instead of guessing.

{
  "name": "Debug Fortran (GDB)",
  "type": "fortran-debug",
  "request": "launch",
  "program": "${command:cmake.launchTargetPath}",
  "args": [],
  "cwd": "${workspaceFolder}",
  "stopAtEntry": false,
  "fortranCompiler": "gnu",
  "MIMode": "gdb"
}

Linux, Intel ifx + GDB

Named as a supported target in PORTABILITY_PLAN.md's objective, but this exact combination (Intel ifx compiled, GDB debugging) is not yet part of the validated matrix the way Windows ifx/cppvsdbg and GNU+GDB are -- see Current Scope And Limits above. Set both fortranCompiler and MIMode explicitly; on Linux neither is inferred for you, even for a fully-formed fortran-debug configuration (see Troubleshooting below):

{
  "name": "Debug Fortran (Intel ifx, GDB)",
  "type": "fortran-debug",
  "request": "launch",
  "program": "${command:cmake.launchTargetPath}",
  "args": [],
  "cwd": "${workspaceFolder}",
  "stopAtEntry": false,
  "fortranCompiler": "intel",
  "MIMode": "gdb"
}

macOS, GNU Fortran + LLDB (experimental)

An unverified feasibility target, not a tested or supported configuration -- see Current Scope And Limits above.

{
  "name": "Debug Fortran (LLDB)",
  "type": "fortran-debug",
  "request": "launch",
  "program": "${command:cmake.launchTargetPath}",
  "args": [],
  "cwd": "${workspaceFolder}",
  "stopAtEntry": false,
  "fortranCompiler": "gnu",
  "MIMode": "lldb"
}

Launch Properties

The extension contributes these Fortran-specific launch properties:

  • fortranIfxSymbolFile
    • Explicit path to fortran_symbols.json
  • fortranIfxScopeWalkEnabled
    • Override scope-walk fallback for this launch
  • fortranIfxScopeWalkMaxDepth
    • Override maximum scope-walk depth for this launch

Standard cpptools launch fields such as program, args, cwd, environment, externalConsole, MIMode, miDebuggerPath, and setupCommands are passed through as expected.

Recommended Workflow For Shared Repos

If your project uses a shared CMakeLists.txt, the recommended setup is to leave CMake alone and keep the debugger integration user-local:

  1. Build the project normally.
  2. Install this extension and ms-vscode.cpptools.
  3. Open the workspace in VS Code.
  4. Run Fortran Debug: Create Fortran Launch Config and pick the compiler and debugger you built with.

That keeps the build system independent of editor tooling and avoids adding extension-specific logic for users who will never need it.

Workspace Settings

The extension contributes these settings:

  • fortranIfx.scopeWalk.enabled (default true)
  • fortranIfx.scopeWalk.maxDepth (default 4, range 1–10)
  • fortranIfx.factsFile (default empty — search the usual paths)
  • fortranIfx.mcpServer.enabled (default true)

These control whether scope-walk fallback is used and how deep it searches when direct expression evaluation fails. They are read when a launch configuration is resolved, so a change takes effect on the next debug session. The matching fortranIfxScopeWalkEnabled / fortranIfxScopeWalkMaxDepth launch properties override them for a single configuration.

Symbol Map Behavior

The proxy looks for fortran_symbols.json in this order:

  1. fortranIfxSymbolFile from the launch configuration, if set
  2. <cwd>/build/debug/fortran_symbols.json
  3. next to the program
  4. one directory above the program
  5. two directories above the program

If no symbol map is found at any of those paths, the proxy makes a single attempt per session to run tools/generate_fortran_symbols.py. That attempt starts when the session launches, alongside the backend starting your program, so it does not stall the first expression you evaluate. Generation needs Python 3 and llvm-nm or nm on PATH; when either is missing the proxy logs the failure and carries on with its other fallbacks.

Run Fortran Debug: Refresh Symbol Map to regenerate the map mid-session after a rebuild, or generate it yourself:

python tools/generate_fortran_symbols.py <search-dir> --output build/debug/fortran_symbols.json

What To Expect During Debugging

When you debug with fortran-debug, the proxy sits between VS Code and the cpptools backend and patches the main Fortran pain points:

  • evaluate
    • direct backend evaluation first
    • inquiry fallback for allocated, associated, size, shape, lbound, and ubound
    • symbol-map mangling fallback
    • resolved-index and cast fallback for arrays
    • synthetic array fallback for allocatable arrays and sections
    • scope-walk fallback
  • variables
    • repair unreadable top-level or child variables when possible
    • format Fortran CHARACTER values more readably
    • expose synthetic arrays and synthetic derived-type objects
  • setBreakpoints
    • rewrite common Fortran conditional-breakpoint syntax to forms cpptools can evaluate
  • scopes (Intel ifx on Windows only)
    • add an Imported (USE ONLY) scope for module variables Locals leaves out; see below

Variables brought in with use ..., only :

On Windows with ifx, vsdbg's Locals lists a module's variables when the routine uses the whole module (use soil_module), but leaves out the ones it brings in by name (use hru_module, only : hru, ihru) or renames (local => remote) whenever that module is compiled separately, which is how nearly every real project, SWAT+ included, is built. The program uses them normally; the debugger just does not list them, and the bare name can fail in Watch as well.

For those the proxy adds an Imported (USE ONLY) scope under Variables:

  • It lists exactly the routine's ONLY-imported and renamed variables that Locals is missing: nothing Locals already shows, and no parameters, types, or procedures. Where Locals already lists them all (a module compiled in the same file as the routine, say), the scope does not appear.
  • Each value is read through its own module's symbol (HRU_MODULE_mp_IHRU), not the bare name. A name two modules both declare shows the one this routine imports, and a renamed variable shows under the routine's name for it; in Watch, both of those fail. Arrays show their size ([13]) and expand, as they do in Locals.
  • It starts closed and costs nothing until opened. Opening it reads the values; any not read within 1.5 s show a load button instead.
  • "Add to Watch" on a row watches the module symbol, which works where the bare name does not. Visual Studio's own Intel debugger accepts hru_module::ihru; vsdbg in VS Code does not, but accepts the symbol, HRU_MODULE_mp_IHRU.
  • A plain derived-type variable that vsdbg knows only as an untyped address (SWAT+'s w, for one) is read as its declared type, so its fields show and expand. Watch still shows it as {...}.

Limits:

  • Intel ifx on Windows (cppvsdbg) only. GDB resolves these names itself, so gfortran, and ifx with GDB on Linux, are unaffected and never show it.
  • It needs the symbol map (fortran_symbols.json), as the Watch fallback does.
  • Only the routine's own use lines count. A module procedure's names from its module's use ..., only :, or a contained procedure's from its host, are left out: the inner routine may declare the same name itself, and listing the outer variable under it would show the wrong value.
  • Free-form sources only (.f90 and later).
  • The routine is found from the stopped line in the source file on disk, so a file edited heavily since the build can point at the wrong one.

Source Caching And Editing During A Session

Several recovery paths read your Fortran sources: array bounds come from the allocate statement that created the array, and derived-type fields come from the type definition. To keep that off the per-step path, the proxy indexes every allocate statement in the source tree once per session and caches the source-file listing alongside it.

Consequences worth knowing:

  • The index is built when the session launches, not on first use, so the cost is paid while your program is starting.
  • Editing sources mid-session invalidates the index through a recursive filesystem watcher. Recursive watching is not available on every platform (notably Linux), and where it is missing the index simply lives for the rest of the session — rebuild and restart the session to pick up changes.
  • Bounds resolved against a stopped frame's live variables are not cached across steps; only the source facts are. Stepping always re-resolves values.

Troubleshooting

The MCP tools report the compiler and debug profile as "unknown", or say there is no active stopped frame, even though a run is sitting at a breakpoint. This almost always means the session in the Run and Debug view is not actually a fortran-debug session. The extension only ever tracks a session whose launch configuration has "type": "fortran-debug" -- a plain cppdbg or cppvsdbg configuration (for example one generated by CMake Tools, or a hand-written one that predates this extension) runs perfectly well on its own, but this extension's proxy, its evaluate/array/breakpoint recovery, and every MCP tool (debug_variables, debug_evaluate, start_fortran_debug, ...) never see it: they have nothing to attach to. Check the "type" field of whichever configuration you launched. If it says cppdbg or cppvsdbg instead of fortran-debug, that is the entire problem -- change the type, or generate a fresh entry with Fortran Debug: Create Fortran Launch Config, and start debugging again through that configuration (ideally via the start_fortran_debug MCP tool, so the session it starts is the one the tools are already watching).

A fortran-debug configuration still reports the compiler or debugger as "unknown". Nothing beyond Windows-without-MIMode is inferred -- deliberately, per PORTABILITY_PLAN.md's rule against guessing compiler or debugger from the OS alone. On Linux or macOS, or whenever MIMode is already present for another reason, set both MIMode ("gdb" or "lldb") and, if you are not using GNU Fortran, fortranCompiler: "intel" explicitly in the launch configuration -- see Launch Configuration above for a complete example per platform/compiler combination. Create Fortran Launch Config writes both fields correctly for every profile it offers; a config that predates a given profile option, or one edited by hand, may be missing one.

Diagnostics

The proxy runs as a separate process and cannot write to the extension's output channel, so its diagnostics go to a log file. Open it with Fortran Debug: Open Proxy Log, or find it at fortran-inspect-proxy.log in your temp directory (%TEMP% on Windows).

The extension's own output channel ("Fortran Debug") carries only activation, configuration resolution, and command results.

When an expression does not resolve, the proxy log shows which fallback stage gave up. Lines worth knowing:

Log line What it tells you
cache priming settled elapsed= Symbol map and allocate index are ready
symbol map loaded path= entries= The map was found and how many symbols it has
symbol map generation failed The generator ran but did not succeed
allocate index built mode= files= statements= Source index is ready; mode=sync means something needed it before priming finished
evaluate direct success The backend resolved it; no fallback needed
evaluate fallback expr= Direct evaluation failed; the fallback chain starts
array bounds source scan expr= candidates= matches= How many allocate statements matched an array
scope walk expr= reads= skipped= timedOut= Containers opened, repeats skipped, and whether the walk hit its budget

A scope walk is bounded to 750ms; timedOut=true on a lookup you expect to succeed means the walk ran out of budget rather than the variable being absent.

Project Layout Notes

Source-assisted features such as array-bound recovery, derived-type field recovery, and some inquiry fallbacks work best when the proxy can infer a source tree from the symbol-map location. In the current implementation, the best-supported layout is:

<workspace>/
  src/
  build/
    debug/
      fortran_symbols.json

If your layout differs, the extension can still work, but some source-parsed recovery paths may be weaker unless you provide an explicit symbol-file path.

Development

npm install
npm run compile
npm run watch
npm run lint
npm test

Press F5 in VS Code to launch an Extension Development Host and test the extension from source.

Layout

The extension host and the proxy are separate processes and do not share state. Only the extension-host side may import vscode.

src/
  extension.ts                 activation, command registration
  fortranDebugProvider.ts      launch-config resolution and defaults
  fortranDebugAdapterFactory.ts  backend selection, spawns the proxy
  logger.ts                    extension-host output channel

  proxyAdapter.ts              the proxy: DAP interception and fallbacks
  dapFraming.ts                DAP wire framing
  dapTypes.ts                  shared type definitions
  proxyConstants.ts            tunables (budgets, limits, feature flags)

  allocateIndex.ts             session index of `allocate` statements
  scopeWalkGuard.ts            scope-walk budget and visited set
  asyncPool.ts                 bounded-concurrency file reads
  expressionHelpers.ts         Fortran expression parsing and formatting
  fortranSourceParser.ts       source line/comment handling
  fortranDeclarationHelpers.ts declaration parsing
  arraySectionHelpers.ts       array-section arithmetic
  evaluateHelpers.ts           DAP value heuristics
  breakpointRewriter.ts        Fortran condition rewriting

  tamanduaFacts.ts             reader for a Tamandua facts file
  breakpointPlanner.ts         variable -> conditional breakpoint sites
  breakpointService.ts         places them via the VS Code debug API
  debugSession.ts              drives a session; reads frames and variables
  debugFormat.ts               compact rendering of a stopped frame
  mcpServer.ts                 loopback MCP server (JSON-RPC over HTTP)
  startupHelpers.ts            argument parsing, launch settings
  symbolMapTools.ts            symbol-map generation
  runtimePaths.ts              log and symbol-map paths
  workspaceLaunchSeed.ts       launch-config discovery (not yet wired up)

proxyAdapter.ts holds the stateful DAP machinery. Pure logic lives in the smaller modules so it can be unit tested — test/ runs against the compiled output in out/ with no test framework, via node test/run-tests.js. A test module either asserts at require time or exports a promise the runner awaits. Adding a test means creating test/<name>.test.js and listing it in test/run-tests.js.

Tunables live in proxyConstants.ts: scope-walk and variable-repair budgets, indexed page size, search depths, and flags to disable variable repair or the watch-evaluate fallback when isolating a problem.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft