Fortran ifx Debug
Fortran ifx 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-ifx debug type and focuses on the cases where plain
cppvsdbg or cppdbg are weak for Fortran:
- module-variable lookup through Intel ifx mangled symbols
- 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 uses cppvsdbg for native ifx debugging. On
Linux/macOS, it routes through cppdbg with GDB or LLDB.
What The Extension Actually Adds
- A
fortran-ifx 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:
- symbol-map based mangling through
fortran_symbols.json
- scope-walk fallback for module and
USE-associated variables
- 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
- Automatic generation of
fortran_symbols.json at launch, when the helper
script and its prerequisites are available.
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.
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:
- Open VS Code.
- Run
Extensions: Install from VSIX....
- Select the
.vsix file for this extension.
- Make sure
ms-vscode.cpptools is also installed.
Quick Start
- Build your Fortran project in a debug configuration.
- Open the workspace in VS Code.
- Add a launch configuration with
"type": "fortran-ifx".
- 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 ifx category in
the command palette:
| Command |
Purpose |
Create Windows Launch Config |
Add or update a Windows ifx entry in launch.json |
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 Windows Launch Config adds or updates a Ugs Debug (Windows IFX)
entry in .vscode/launch.json for the active workspace folder, using the
generic ${command:cmake.launchTargetPath} program pattern. It writes through
the VS Code configuration API, which rewrites the configurations block, so
comments in launch.json are not preserved.
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 ifx: 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:
fortranIfx.factsFile, if set
swatplus-facts.json in the workspace folder
build/swatplus-facts.json
build/debug/swatplus-facts.json
.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 ifx: 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 |
Evaluate 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 |
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 -- and
debug_crash_report arrived in 0.10.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.
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.
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 ifx: 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 ifx 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
Windows ifx
Use cppvsdbg implicitly by omitting MIMode:
{
"name": "Ugs Debug (Windows IFX)",
"type": "fortran-ifx",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"environment": [],
"externalConsole": false
}
Linux Or macOS With GDB
{
"name": "Debug Fortran (GDB)",
"type": "fortran-ifx",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"MIMode": "gdb"
}
Linux Or macOS With LLDB
{
"name": "Debug Fortran (LLDB)",
"type": "fortran-ifx",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"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:
- Build the project normally.
- Install this extension and
ms-vscode.cpptools.
- Open the workspace in VS Code.
- Run
Fortran ifx: Create Windows Launch Config.
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:
fortranIfxSymbolFile from the launch configuration, if set
<cwd>/build/debug/fortran_symbols.json
- next to the program
- one directory above the program
- 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 ifx: 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-ifx, 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
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.
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 ifx: 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 ifx 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.