# Documentation Snippet Validation
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Tools](#tools)
- [C / C++ validator](#c-c-validator)
- [C / C++ analyser](#c-c-analyser)
- [Rust validator](#rust-validator)
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [C / C++ samples](#c-c-samples)
- [Options](#options)
- [C / C++ block analyser](#c-c-block-analyser)
- [Options](#options-1)
- [Rust samples](#rust-samples)
- [Options](#options-2)
- [How It Works — C / C++](#how-it-works-c-c)
- [Step 1 — Extraction and classification (`doc-c-samples.py`)](#step-1-extraction-and-classification-doc-c-samplespy)
- [Step 2 — Compilation (`doc-c-samples.sh`)](#step-2-compilation-doc-c-samplessh)
- [Step 3 — Combined retry](#step-3-combined-retry)
- [Step 4 — Reporting](#step-4-reporting)
- [Fragment Wrapper](#fragment-wrapper)
- [How It Works — Rust](#how-it-works-rust)
- [Step 1 — Extraction and classification (`doc-rust-samples.py`)](#step-1-extraction-and-classification-doc-rust-samplespy)
- [Step 2 — Type-checking (`doc-rust-samples.sh`)](#step-2-type-checking-doc-rust-samplessh)
- [Step 3 — Combined retry](#step-3-combined-retry-1)
- [Step 4 — Reporting](#step-4-reporting-1)
- [Rust Fragment Wrapper](#rust-fragment-wrapper)
- [Adding New Documentation Snippets](#adding-new-documentation-snippets)
- [C / C++ snippets](#c-c-snippets)
- [Rust snippets](#rust-snippets)
This directory contains tooling that extracts every C, C++, and Rust code block
from the Synta documentation, compiles each one with a real compiler, and
reports failures with a precise source location.
## Tools
### C / C++ validator
| `doc-c-samples.sh` | Driver: collects markdown files, invokes the extractor, compiles each block, prints results |
| `doc-c-samples.py` | Extractor: parses markdown, classifies and wraps each block, writes compilable source files, a compile manifest, and a skipped manifest |
### C / C++ analyser
| `doc-c-analyze.sh` | Driver: collects markdown files, invokes the extractor, then shows a classification analysis of every block |
| `doc-c-analyze.py` | Reporter: reads the manifests produced by the extractor and formats the analysis output |
### Rust validator
| `doc-rust-samples.sh` | Driver: collects markdown files, invokes the extractor, type-checks each block via a temporary Cargo project, prints results |
| `doc-rust-samples.py` | Extractor: parses markdown, classifies and wraps each block, writes compilable source files and a manifest |
All scripts must reside in the same directory. Each shell script locates its
companion Python script relative to itself, so they can be invoked from any
working directory.
## Prerequisites
- **Bash 4+** — required by all shell scripts. macOS users may need to install a newer bash via Homebrew (`brew install bash`), as macOS ships with bash 3.2. Each script checks the version on startup and exits with an error if bash 3 or earlier is detected.
- **Rust toolchain** — needed to build `synta-ffi` (C validator) and for `cargo check` (Rust validator)
- **gcc** (or another C compiler) and **g++** (or another C++ compiler) — for the C validator
- **python3** ≥ 3.9
Build the FFI crate once before the first run of the C validator:
```sh
cargo build -p synta-ffi
```
The Rust validator builds synta automatically on its first run.
## Quick Start
### C / C++ samples
Validate every workspace markdown file (default):
```sh
./contrib/validation/doc-c-samples.sh
```
Validate a single file and see every block (not just failures):
```sh
./contrib/validation/doc-c-samples.sh -v docs/C_API.md
```
Restrict the search to a single directory:
```sh
./contrib/validation/doc-c-samples.sh --docs-dir docs/
```
Use a different compiler:
```sh
./contrib/validation/doc-c-samples.sh --cc clang --cxx clang++
```
#### Options
| `--docs-dir DIR` | workspace-wide | Directory searched for `*.md` files instead of scanning the whole workspace |
| `--header-dir DIR` | `include/` | Directory containing `synta.h` |
| `--cc COMPILER` | `$CC` or `gcc` | C compiler |
| `--cxx COMPILER` | `$CXX` or `g++` | C++ compiler |
| `--verbose`, `-v` | off | Print a result line for every block, not just failures |
| `FILE.md ...` | — | Process only these files instead of scanning the workspace |
### C / C++ block analyser
Show why each block in `docs/*.md` was classified, with a preview of every
skipped block:
```sh
./contrib/validation/doc-c-analyze.sh
```
Analyse a single file, also listing compilable blocks:
```sh
./contrib/validation/doc-c-analyze.sh --show-compilable docs/C_API.md
```
The analyser never invokes a compiler — it is a pure classification report,
useful when adding new documentation snippets or investigating skipped blocks.
#### Options
| `--docs-dir DIR` | `docs/` | Directory searched for `*.md` files |
| `--show-compilable` | off | Also list every compilable block, not just skipped ones |
| `FILE.md ...` | — | Process only these files instead of the whole docs directory |
### Rust samples
Validate every workspace markdown file (default):
```sh
./contrib/validation/doc-rust-samples.sh
```
Validate a single file and see every block (not just failures):
```sh
./contrib/validation/doc-rust-samples.sh -v docs/usage.md
```
Restrict the search to a single directory:
```sh
./contrib/validation/doc-rust-samples.sh --docs-dir docs/
```
Also compile serde integration blocks (requires the `serde` feature):
```sh
./contrib/validation/doc-rust-samples.sh --features-serde
```
#### Options
| `--docs-dir DIR` | workspace-wide | Directory searched for `*.md` files instead of scanning the whole workspace |
| `--features-serde` | off | Also compile `serde_json` blocks (enables `synta/serde`) |
| `--verbose`, `-v` | off | Print a result line for every block, not just failures |
| `FILE.md ...` | — | Process only these files instead of scanning the workspace |
Set `NO_COLOR=1` to suppress ANSI colour codes in the output.
Exit status is **0** if all blocks compiled successfully, **1** if any failed.
## How It Works — C / C++
### Step 1 — Extraction and classification (`doc-c-samples.py`)
The Python script reads each markdown file and collects every fenced code block
whose language tag is `c`, `cpp`, or `c++`. Each block is then classified:
| `skip_nonsynta` | No `synta_` / `Synta*` identifier | Skip — not a Synta API example |
| `skip_foreign` | OpenSSL or libtasn1 API calls detected | Skip — migration comparison code |
| `skip_pitfall` | Block contains both `// WRONG` and `// RIGHT` markers | Skip — intentional anti-pattern, compiler would flag the wrong half |
| `skip_pseudocode` | Block uses `...` as a function-call argument | Skip — illustrative placeholder, not real C |
| `skip_header_doc` | Block reproduces `synta.h` type or function declarations | Skip — would cause redeclaration errors alongside `#include <synta.h>` |
| `program` | Block contains `int main(` | Compile as a self-contained translation unit |
| `toplevel` | Block starts with a function/struct/typedef definition | Compile at file scope |
| `fragment` | Anything else | Wrap in a harness function with pre-declared variables |
For each compilable block the extractor writes two files into a temporary
directory:
- **`src/NNNNN.{c,cpp}`** — the wrapped, ready-to-compile translation unit
- **`src/NNNNN.combined.{c,cpp}`** — all preceding `toplevel`/`program` blocks
from the same doc file prepended before the current block (generated only
when there are preceding blocks); used for the retry step below
A manifest (`manifest.tsv`) lists one compilable block per line with seven
tab-separated fields:
```
doc_file start_line lang src_file kind raw_file combined_file
```
Only compilable blocks appear in the manifest; skipped blocks are excluded
entirely to avoid tab-field collapsing when bash reads the TSV.
### Step 2 — Compilation (`doc-c-samples.sh`)
The shell script reads the manifest and runs the compiler with
`-fsyntax-only` (full type-check, no object file) on each source file.
**Compiler flags applied to every block:**
```
-std=c11 / -std=c++17
-Wall -Wextra -Wformat=2
-Werror=implicit-function-declaration (catches misspelled function names)
-Werror=incompatible-pointer-types (catches wrong argument types)
-Werror=int-conversion (catches integer/pointer confusion)
-Werror=format (catches wrong printf specifiers)
-Wno-unused-variable -Wno-unused-but-set-variable
-Wno-unused-function -Wno-unused-parameter -Wno-shadow
```
Include paths: `include/` (for `synta.h`) and `synta-ffi/examples/c/` (for
generated headers such as `certificate.h`).
### Step 3 — Combined retry
If a block fails to compile standalone, the script retries with the combined
file (all preceding `toplevel`/`program` blocks of the same doc file
prepended). This handles the common case where an implementation block
references a struct type defined in a preceding header block of the same
document. Local `#include "..."` lines are stripped from the current block
before combining, since the type definitions are already inlined from the
preceding blocks.
### Step 4 — Reporting
Each failure prints the originating file and line number, the block kind, and
the compiler error with the temporary file path replaced by the doc reference:
```
[FAIL] docs/C_API.md:142
Language : c
Kind : fragment
Errors:
<docs/C_API.md:142>:5:5: error: ...
```
A summary line shows the total pass / fail / skip counts.
## Fragment Wrapper
Standalone statement blocks (`kind = fragment`) are wrapped in a harness
function so that declarations in the snippet can be type-checked without a
surrounding function body:
```c
#include <synta.h>
#include <stdio.h> /* + other standard headers */
static int _synta_doc_sample(void) {
/* ~50 pre-declared identifiers covering all variables used in doc snippets */
SyntaDecoder *decoder = NULL;
SyntaEncoder *encoder = NULL;
SyntaByteArray output = {NULL, 0, 0};
/* ... */
/* ── fragment (nested scope allows re-declaration) ── */
{
/* the raw code block goes here */
}
return 0;
}
```
The nested `{ }` scope allows the fragment to re-declare variables (e.g.
`SyntaDecoder *decoder = ...`) without clashing with the outer pre-declarations.
## How It Works — Rust
### Step 1 — Extraction and classification (`doc-rust-samples.py`)
The Python script reads each markdown file and collects every fenced code block
whose language tag is `rust` (with or without annotations). Each block is
classified:
| `skip_annotated` | Block has `ignore` or `compile_fail` annotation | Skip — explicitly not compilable |
| `skip_nonsynta` | No `synta` identifier found | Skip — not a Synta API example |
| `skip_nostd` | Block has `#![no_std]` | Skip — needs a cross-compilation target |
| `skip_foreign` | Uses a foreign ASN.1 crate (`der`, `asn1-rs`, `yasna`) | Skip — migration comparison code |
| `skip_serde` | Uses `serde_json` and `--features-serde` was not passed | Skip — needs serde feature |
| `program` | Block contains `fn main()` | Rename main, compile as a library crate |
| `toplevel` | Block starts with `fn`, `struct`, `impl`, `type`, etc. | Place at file scope |
| `fragment` | Anything else | Wrap in a harness function with pre-declared variables |
Blocks with annotation `no_run` (compile but do not execute) are still compiled.
For each compilable block the extractor writes two files:
- **`src/NNNNN.rs`** — the wrapped, ready-to-compile translation unit
- **`src/NNNNN.combined.rs`** — all preceding `toplevel`/`program` blocks from
the same doc file prepended before the current block (generated only when
there are preceding blocks); used for the retry step below
A manifest (`manifest.tsv`) lists one compilable block per line with seven
tab-separated fields:
```
doc_file start_line lang src_file kind raw_file combined_file
```
### Step 2 — Type-checking (`doc-rust-samples.sh`)
The shell script creates a temporary Cargo project whose `Cargo.toml` declares
synta as a path dependency. Using the workspace's shared `target/` directory
means synta and its transitive dependencies are compiled at most once, keeping
per-snippet overhead to just re-analysing the tiny `src/lib.rs`.
For each snippet the script writes the wrapped source to
`$CRATE_DIR/src/lib.rs` and runs `cargo check --quiet`.
### Step 3 — Combined retry
If a snippet fails to check standalone, the script retries with the combined
file (all preceding `toplevel`/`program` blocks prepended at file scope).
This handles the case where a snippet references a type or helper function
defined in an earlier block of the same document.
### Step 4 — Reporting
Each failure prints the originating file and line number, the block kind, and
the compiler error with the generated file path replaced by the doc reference:
```
[FAIL] docs/usage.md:129
Language : rust
Kind : fragment
Errors:
<docs/usage.md:129>:5:5: error: ...
```
A summary line shows the total pass / fail / skip counts.
## Rust Fragment Wrapper
Standalone statement blocks (`kind = fragment`) are wrapped in a harness
function that returns `std::result::Result` (fully-qualified to avoid shadowing
by `synta::Result<T>`) so that the `?` operator works without any annotation:
```rust,ignore
#![allow(unused_imports, unused_variables, /* … */)]
use synta::*;
fn _synta_doc_sample() -> std::result::Result<(), Box<dyn std::error::Error>> {
// ── pre-declared identifiers for doc fragment compilation ──
let data: Vec<u8> = vec![0x02, 0x01, 0x2a];
let mut decoder = synta::Decoder::new(data.as_slice(), synta::Encoding::Der);
let mut encoder = synta::Encoder::new(synta::Encoding::Der);
let integer = synta::Integer::from(0i64);
// ── fragment (nested scope allows re-declaration) ──
{
/* the raw code block goes here */
}
Ok(())
}
```
The nested `{ }` scope allows the fragment to re-declare any of the
pre-declared identifiers (e.g. `let mut decoder = ...`) via shadowing rather
than causing a duplicate-binding error.
## Adding New Documentation Snippets
### C / C++ snippets
When you add a new C or C++ code block to the docs:
1. Run the validator to confirm it compiles:
```sh
./contrib/validation/doc-c-samples.sh -v docs/YOUR_FILE.md
```
2. If a variable used by your snippet is not among the pre-declared identifiers
in `FRAGMENT_VARS` (in `doc-c-samples.py`), add it there.
3. If your snippet deliberately shows incorrect code alongside correct code,
mark both sections with `// WRONG` and `// RIGHT` comments — the validator
will skip the block automatically.
4. If your snippet is pseudocode (uses `...` as an argument placeholder),
no action is needed; it is skipped automatically.
### Rust snippets
When you add a new Rust code block to the docs:
1. Run the validator to confirm it type-checks:
```sh
./contrib/validation/doc-rust-samples.sh -v docs/YOUR_FILE.md
```
2. If a variable used by your snippet is not among the pre-declared identifiers
in `FRAGMENT_VARS` (in `doc-rust-samples.py`), add it there.
3. If your snippet uses `serde_json`, confirm it compiles with the serde flag:
```sh
./contrib/validation/doc-rust-samples.sh --features-serde -v docs/YOUR_FILE.md
```
4. If your snippet uses `#![no_std]`, it is skipped automatically (it would
require a bare-metal cross-compilation target).
5. If your snippet is a migration comparison that also imports a foreign crate
(`der`, `asn1-rs`, `yasna`), it is skipped automatically.
6. If your snippet deliberately shows code that should not compile (an
anti-pattern or error case), add the `compile_fail` annotation to the
fenced block opener:
````markdown
```rust,compile_fail
// intentionally wrong code
```
````