Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>P4 LanguageNew to Visual Studio Code? Get it now.
P4 Language

P4 Language

Ferox Hosting

|
3 installs
| (0) | Free
Language server for P4_16: semantic highlighting, parser-state-aware completion, spec hover docs, go-to-definition, and P4-specific lints.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

P4 Language

DISCLAIMER: Large parts of this codebase and README are LLM-generated. They may be wrong or contain bugs. Every release is tested extensively by a real human.

A Visual Studio Code language server for P4₁₆, built around a hand-written parser that keeps working on code that does not compile — which is when an editor earns its keep.

Tuned for v1model / BMv2 programs, but nothing is hard-coded to it: other architectures work from whatever they #include.

Nothing needs to be installed. The p4lang standard library headers ship with the extension, so #include <core.p4> and #include <v1model.p4> resolve on a machine with no compiler on it — which is the common case when the compiler lives in a VM or a container.

What it does

Highlighting, in two layers

A TextMate grammar covers the file before the server starts — every reserved keyword split by role, the contextual table properties (key, actions, default_action, …) recognised only inside a table, match kinds only inside a key block, and every P4 numeric literal form including 8w0xFF, 16s-10 and 32w0xdead_beef.

On top of it, semantic tokens colour what a grammar cannot know: a header type apart from a struct apart from a control, parser states as their own kind, parameter directions, const as readonly, everything from core.p4 and v1model.p4 marked defaultLibrary, @deprecated symbols struck through, and #if-excluded regions greyed out.

Completion that knows where the cursor is

Completion is driven by syntactic position, not prefix matching.

Where you are What you get
transition ▸ every state in the parser, plus accept and reject and a select snippet — states nothing transitions to yet are ranked first, since those are the ones you are probably wiring up
select(hdr.ethernet.etherType) { ▸ only constants whose width matches the key. After a bit<16> key you get TYPE_IPV4 and TYPE_MYTUNNEL, not every constant in the file. Labels already used by another arm are dropped, and default disappears once an arm has one
…: ▸ inside a select state names, ranked so the ones this select does not target yet come first
packet.extract(▸ the headers of the out headers parameter, with the not-yet-extracted ones first
hdr., standard_metadata. the real members of the resolved type, each with its bit width
key = { x: ▸ } match kinds, exact and lpm first
actions = { ▸ } actions of this control that are not already listed, plus NoAction
default_action = ▸ only the actions this table lists, with a parameter snippet
inside a table body the properties not yet present
#include <▸ files from the resolved include path
@▸ the predefined annotations

Hover

Three sources merged:

  • The program itself. A header type reports its total width and whether it is byte-aligned; a field reports its width and bit offset within the header; a table reports its key, actions, and its control-plane name (MyIngress.ipv4_lpm) — the exact string that goes in sN-runtime.json; a parser state reports what it transitions to; a macro reports what it expands to.
  • Curated v1model notes. standard_metadata.egress_spec explains that it is the one you assign in ingress and how it differs from the read-only egress_port. Around 60 entries cover standard_metadata_t, the extern functions, register/counter/meter, the HashAlgorithm values, and the built-in header methods.
  • The specification. Sixty-five constructs are linked to their section of the P4₁₆ spec, mined at build time from the HTML.

Navigation

Go-to-definition (including into core.p4 and v1model.p4, to a state from a transition, to a #define from a macro use, and to the file from an #include), find-all-references, document highlight, scope-correct rename that refuses to collide with an existing name or to edit the bundled standard library, a hierarchical outline, workspace symbols, and folding.

Diagnostics

Everything below is a mistake p4c accepts.

Parser state flow

  • a state unreachable from start — the classic "wrote the state, never wired up the select arm"
  • a state from which accept/reject can never be reached
  • a state with no transition
  • a select with no default arm, so unmatched packets are rejected
  • an arm after default, or two arms with the same label
  • the same header extracted twice in one state

Header validity and the deparser

  • a header parsed but never emitted, so it is silently stripped from the outgoing packet — the drop-a-line-from-the-deparser bug
  • an emit order that contradicts the order the parser extracts in
  • a field read from a header that is only valid on some parser paths, without an isValid() guard
  • an emit of a header nothing ever makes valid

The validity analysis is deliberately quiet: it understands that update_checksum(hdr.ipv4.isValid(), …) guards its other arguments, and that an action reached only from a table applied inside if (hdr.ipv4.isValid()) is safe. Real programs produce zero or one warning, not one per field.

Names and types — unresolved names, a name used before it is declared (p4c's declaration not found; only parser states may be referenced early), unknown members, a default_action that is not in the table's actions list, a switch label that is not an action of the switched table, duplicate declarations, a missing ;, and a return/exit/switch/transition where the spec forbids it.

Optional p4c

Set p4.compiler.enable to also run the real compiler on save and merge its errors. It probes once for the binary and stays silently disabled when there isn't one, so a machine that compiles inside a VM never sees a prompt.

Settings

Setting Default
p4.includePaths [] extra #include directories; the file's own directory and the bundled p4include/ are always searched
p4.diagnostics.parserStates true the parser state flow checks
p4.diagnostics.headerValidity true the deparser and validity checks
p4.maxNumberOfProblems 200
p4.compiler.enable false also run p4c on save and merge its errors
p4.compiler.path p4c-bm2-ss the compiler executable, found on PATH or given as an absolute path
p4.compiler.args ["--p4v", "16"] extra arguments passed to it
p4.compiler.runOn save save or never
p4.trace.server off LSP tracing

In an untrusted workspace the three p4.compiler.* settings are ignored, since they name a program to run; everything else works unchanged.

Development

Node 20+.

npm install
npm run build          # bundle client + server with esbuild
npm run watch          # ... and keep bundling
npm run check-types    # tsc --noEmit on both workspaces
npm test               # unit + corpus + feature tests
npm run gen:docs       # regenerate spec-docs.json from the spec HTML
npm run package        # build a .vsix

Press F5 for an Extension Development Host with example1.p4 open. The Extension + Server compound also attaches a debugger to the language server.

How it fits together

document text
  → preprocess   #include as a scope import (never a textual splice),
                 macro expansion with provenance, #if evaluation
  → lex          tokens with exact spans and attached trivia; lossless
  → parse        recursive descent, error recovery, no symbol table
  → bind         scope tree, symbol table, cross-file through the include graph
  → analyse      parser state graph, header validity lattice
  → features     hover / completion / definition / tokens / diagnostics

Three decisions shape the rest:

The parser never consults a symbol table. p4c resolves the grammar's type-name ambiguity by feeding the parser's symbol table back into the lexer. That is untenable in an editor, where the symbol table is always a keystroke out of date. The ambiguous positions are parsed speculatively instead, and any residual ambiguity is recorded on the node for name resolution to settle.

The parser never throws. An unparseable construct becomes an ErrorNode and parsing resumes at the next declaration boundary. Completion is requested precisely when the file does not parse — transition with nothing after it — so a parser that gives up has nothing to offer.

#include is a scope import, not a textual splice. The included file is parsed as its own document and its top-level scope becomes an import edge. The edited file's positions stay exact, and go-to-definition into core.p4 falls out for free.

Tests

npm test runs 186 tests, including a corpus gate that parses and binds every file in p4include/ (core.p4, v1model.p4, psa.p4, pna.p4 and the rest) plus the lab programs vendored under test/fixtures/labs/, asserting zero syntax errors and zero unresolved names. The control-plane name computation is checked against a real p4c-generated .p4info.txt kept next to the program it was built from. The suite is self-contained: nothing outside the repository is read.

Licence

Apache-2.0 — see LICENSE.

Two things are bundled from elsewhere, both Apache-2.0 and both credited in NOTICE: the standard library headers in p4include/, vendored unmodified from p4lang/p4c, and the specification excerpts shown on hover, extracted from the P4₁₆ language specification published by the P4 Language Consortium.

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