# FoXy v0.8.0 is out

> Source: <https://fortran-lang.discourse.group/t/foxy-v0-8-0-is-out/11273#post_1>
> Published: 2026-11-03 17:14:13+00:00

Fortran XML for poor people

FoXy is a pure Fortran library to read, query, edit, and write XML, with no C underneath: `use foxy` and your code talks XML (hopefully). It started as a small parser for another poor people project (VTKFortran); the last four releases (0.5 to 0.8) rebuilt it (with the help of an AI assistant) into what scientific code needs from XML (at least in my mind, even though I prefer other kinds of I/O formats generally): **case files read as numbers, results metadata written as you compute, and files of any size**, including over 2 GiB.

Five types, one for each way of working with a document:

| Type | For | 
|---|---|
| `xml_file` | a whole document in memory (a DOM): parse, query, edit, save | 
| `xml_index` | a file too large to hold: tags and attributes, contents read on demand | 
| `xml_handler` | a document of any size read once: your callbacks, nothing stored | 
| `xml_writer` | a document of any size, written element by element to a file, a unit or memory | 
| `xml_tag` | a single element: build it, parse it, write it | 

The common case: a solver reads its settings from XML. No string juggling, no `read(..., *)`:

```
use foxy
type(xml_file) :: doc
real(R8P)      :: dt
integer(I4P)   :: steps

call doc%parse(filename='case.xml')
call doc%get_content(id=doc%find(path='case/time/dt'), value=dt)
call doc%get_content(id=doc%find(path='case/time/steps'), value=steps)
```

`values=` into an allocatable array), with `iostat`/` iomsg`. A value that is not a number gives `XML_BAD_VALUE` and leaves your variable unchanged; a missing tag or attribute gives `XML_NOT_FOUND`. `1e3` is not accepted as an integer.`parse` returns `XML_MALFORMED` with the first problem and its line: tag structure, names, duplicate attributes, a second root, text outside the root, malformed references, ill-formed UTF-8.

```
ids = doc%select('case/boundary[@type="wall"]')                 ! attribute value
ids = doc%select('//*[@unit]')                                  ! any name, at any depth
ids = doc%select('/case/boundary[2]')                           ! the second boundary child
ids = doc%select('//boundary[@side~="^(east|west)$"]/velocity') ! regular expression
```

`/`, `//`, `*`, `[@a]`, `[@a="v"]`, `[@a~="re"]`, `[n]` among siblings. A malformed expression returns no IDs and the column of the problem in `matching(re, ...)`, using the StringiFor engine (Python `re` syntax, linear time, no backtracking).`find`/` find_all`: by prefix, by `{uri}local`, or with your own prefixes mapped to URIs.

```
type(xml_writer) :: w

call w%open()
call w%start('cell')
call w%attribute('dt', 0.1_R8P)                        ! the shortest text that reads back: 0.1
call w%attribute('extent', [0_I4P, 63_I4P, 0_I4P, 31_I4P])
call w%start('volume')
call w%text(1._R8P/3._R8P)
call w%end('volume')
call w%start('center')
call w%text([0.5_R8P, 0.25_R8P, 0._R8P], fmt='(F5.3)')
call w%end('center')
call w%close()
<cell dt="0.1" extent="0 63 0 31">
  <volume>0.3333333333333333</volume>
  <center>0.500 0.250 0.000</center>
</cell>
```

`0.1`, not `1.000000000000000E-001`), or your own `fmt=`. A value too wide for the field is an error, not a row of asterisks.`& < > " '`, checks names, duplicate attributes, one root element, and PI targets. Errors are sticky and reported once, by `close` or `set_content`/` set_attribute` with `value=`/` add_tag(parent=|before=|after=)`, `set_name`, `delete_tag`, then `save`.

```
type, extends(xml_handler) :: pressure_sum
   ...
   contains
      procedure :: start_element => on_start
      procedure :: characters    => on_text
      procedure :: end_element   => on_end
endtype pressure_sum

call summer%parse(filename='results.xml', iostat=iostat)
```

Extend `xml_handler`, override the callbacks you need (start and end tags, text, CDATA, comments, PIs, DOCTYPE, problems with their line), and the file is read in chunks with nothing stored; `stop_parsing` ends it early. Your callbacks are plain type-bound procedures: I/O and side effects are allowed.

For random access to files too large for a DOM, there is `xml_index`: it scans the tags and attributes only, and reads content from the file when you ask for it (`stop_at=` stops the scan early).

UTF-8, UTF-16, UTF-32 (byte order mark or not), ISO-8859-1, Windows-1252, and US-ASCII sources are detected (XML 1.0 Appendix F, then the declaration) and converted to UTF-8 piece by piece. `save` writes UTF-8 and declares it. An encoding FoXy does not read is reported, not guessed.

What I use it for, and what it is designed for:

`xml_writer` as the run goes, numbers formatted for you. VTKFortran writes its XML formats through FoXy.`xml_index` or `xml_handler` on files of hundreds of MB or over 2 GiB, without loading them.
Performance: parsing is linear in the document size. A generated QuakeML-like catalogue of 100,000 tags (3 MB) is parsed into the DOM in about 0.11 s and written back by `stringify` in 0.03 s (gfortran 16, `-O2`, a desktop CPU; `fobis build --mode benchmark-gnu` reproduces it).

Known limits: entities declared in a DTD are not expanded, and there is no validation against a DTD or schema.

43 test programs, run with gfortran, nvfortran and ifx (debug builds with runtime checks included). Chunked parsing is checked against whole-string parsing down to 1-character chunks, the streaming events against the DOM on every test file, and the index against the DOM tag by tag. Every sample in the documentation is a program compiled and run to produce the output shown, and CI fails if the output changes.

The documentation has a [tutorial](https://fortran-foss-programmers.github.io/FoXy/manual/tutorial/01-first-document) that builds the input and output of a small solver in nine chapters, a [cookbook](https://fortran-foss-programmers.github.io/FoXy/manual/cookbook), and an [Upgrading](https://fortran-foss-programmers.github.io/FoXy/guide/upgrading) page listing the behaviour changes release by release.

FoXy builds with FoBiS, fpm, or CMake, is tested with GNU gfortran, NVIDIA nvfortran, and Intel ifx, and is multi-licensed (GPL v3, BSD 2-Clause, BSD 3-Clause, MIT, select the best fit for you).

Feedback, issues, and contributions are welcome.

Stefano
