Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"attribute:target": "LANGUAGE_SPEC.md",
"attribute:test": "AI_FLOW_HANDBOOK.md",
"attribute:unsafe": "language/README.md",
"backend:bpf_target": "language/bpf.md",
"backend:c_generator": "vision/north-star.md",
"backend:js_generator": "language/modules-namespacing.md",
"backend:metal_codegen": "language/wasm-crossings.md",
Expand Down
114 changes: 114 additions & 0 deletions docs/language/bpf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# eBPF (bpfel) target

Flow compiles to ELF eBPF objects that the Linux kernel can load. The target
reuses the existing MLIR and LLVM pipeline, then applies the BPF ABI and a set
of verifier-oriented restrictions before LLVM's BPF backend emits the object.

Loading, attaching and live verifier runs are out of scope here; they belong to
`flow-kernel`. What this page covers is producing an object the kernel will
accept.

## Compiling a program

```bash
PYTHONPATH=src python3 -m flow.bpf_target tests/fixtures/bpf/socket_filter.flow \
--target bpfel \
--entry socket_filter \
--section socket \
--license GPL \
-O 2 \
-o socket-filter.bpf.o
```

| Flag | Meaning |
|------|---------|
| `-o`, `--output` | Output `.o` path. Required. |
| `--target` | `bpfel` (little-endian BPF). The only target today. |
| `--entry` | The exported Flow function to expose as a BPF program. |
| `--section` | ELF section for the program, for example `socket` or `xdp`. |
| `--license` | License string written to the `license` section. Defaults to `GPL`. |
| `-O` | `0`, `1`, `2` or `3`. Defaults to `2`. |

The input may be a `.flow` file or LLVM IR (`.ll`), which is useful when
inspecting what the pipeline produced before the BPF stage.

A minimal program is an exported function taking the program context:

```flow ignore="compiled by the bpfel target rather than the host toolchain"
export function socket_filter(context: ptr<void>) -> i32 {
return 0
}
```

## What the target guarantees

The BPF ABI is fixed:

| Property | Value |
|---|---|
| Triple | `bpfel` |
| Data layout | `e-m:e-p:64:64-i64:64-i128:128-n32:64-S128` |
| Pointer width | 64 bits |
| Stack limit | 512 bytes, the kernel verifier's limit |

The entry function is placed in the section you name, and the license string is
emitted as a separate `license` section, which is what the kernel reads:

```llvm
define i32 @socket_filter(ptr %0) section "socket" {
@_flow_bpf_license = dso_local constant [4 x i8] c"GPL\00", section "license", align 1
```

## What it rejects

A BPF program runs in the kernel with no userspace runtime, so the target fails
the build rather than emitting an object the verifier will reject. Any reference
to an allocator or to C++ exception machinery is refused:

```
bpfel cannot depend on userspace runtime symbol 'malloc'
```

The full list is `malloc`, `calloc`, `realloc`, `free`, `operator new`, `_Znwm`,
`_Znam`, `__cxa_throw`, `__cxa_allocate_exception` and `_Unwind_`. In practice
this means BPF programs work on the stack and on context pointers, within the
512-byte limit.

## Entry point naming

The target lowers with `--llvm`, so the definition in the IR carries the plain
Flow name. It looks for `flow_export_<entry>` first and falls back to `<entry>`.

That order matters. The C backend emits both a mangled definition and a visible
`flow_export_<name>` alias for `--export`, so when both are present the alias is
the one to decorate; the MLIR path emits only the plain name. An earlier version
searched only for the alias, which the MLIR path never produces, so every build
failed with `exported BPF entry not found`.

## Toolchain

The clang on `PATH` needs LLVM's BPF backend. Apple's system clang does not have
it and fails with:

```
error: unable to create target: 'No available targets are compatible with triple "bpfel"'
```

This is the same limitation as the direct wasm32 target. On Linux, an LLVM 18
toolchain works; `.github/workflows/bpf.yml` installs one.

## What CI verifies

`.github/workflows/bpf.yml` compiles both fixtures, then checks the object
rather than trusting the exit code: `llvm-readelf` must report a BPF machine
type, and `llvm-objdump` must show the named program section and the `license`
section.

Unit coverage for the ABI contract, the forbidden-symbol check and the metadata
decoration lives in `tests/unit/test_bpf_target.py`.

## Related

- [WebAssembly](wasm.md) — the other freestanding target, with the same
toolchain caveat
- [MLIR opt flags](mlir-opt-flags.md) — the optimizer this path runs through
4 changes: 4 additions & 0 deletions docs/nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,10 @@
"label": "WebAssembly",
"path": "language/wasm.md"
},
{
"label": "eBPF",
"path": "language/bpf.md"
},
{
"label": "WASM Crossings",
"path": "language/wasm-crossings.md"
Expand Down
11 changes: 8 additions & 3 deletions scripts/check_doc_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,17 @@ def _stdlib_modules() -> set[str]:
}


# Suffixes a backend module can carry. `_target` is here because a BPF target
# on an unmerged branch would otherwise have landed as a whole new compilation
# target that the coverage gate could not see.
_BACKEND_SUFFIXES = ("_generator", "_codegen", "_compiler", "_target", "_backend")


def _backends() -> set[str]:
names = set()
for path in (ROOT / "src" / "flow").glob("*.py"):
stem = path.stem
if stem.endswith(("_generator", "_codegen", "_compiler")):
names.add(stem)
if path.stem.endswith(_BACKEND_SUFFIXES):
names.add(path.stem)
return names


Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_doc_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ def test_backends_are_discovered():
assert {"c_generator", "mlir_generator", "wasm_compiler"} <= cov._backends()


def test_backend_discovery_covers_every_naming_convention(tmp_path, monkeypatch):
"""A new target must not slip past the gate because of its filename.

A BPF target on an unmerged branch was named bpf_target.py, which the
original three suffixes did not match, so a whole new compilation target
would have landed undocumented.
"""
src = tmp_path / "src" / "flow"
src.mkdir(parents=True)
for name in ("x_generator.py", "y_codegen.py", "z_compiler.py",
"bpf_target.py", "w_backend.py", "parser.py"):
(src / name).write_text("")
monkeypatch.setattr(cov, "ROOT", tmp_path)
found = cov._backends()
assert found == {"x_generator", "y_codegen", "z_compiler",
"bpf_target", "w_backend"}


# --------------------------------------------------------------------------
# The shipped map
# --------------------------------------------------------------------------
Expand Down
Loading