Files
EMBS/Practicals/hlsdev/README.md

6.2 KiB

Local HLS development (hlsdev)

Vitis is slow over X11, so this folder lets you edit, check and test the practical code on your Mac, and only go to the server when you want a real synthesis run.

It gives you four things:

What Why
1 clangd diagnostics for the C++ itself Real errors and warnings as you type, with no Xilinx install
2 #pragma HLS highlighting Stock syntax files treat pragmas as anonymous preprocessor text
3 #pragma HLS linting Compilers silently ignore misspelled pragmas - this catches them
4 A local C-simulation (./sim.sh) Runs the real testbench in about a second instead of minutes

The one thing worth understanding first

#pragma HLS ... is not C++. clang and gcc have never heard of it and just ignore it:

#pragma HLS PIPELINE II=2
#pragma HLS TOTALLY_MADE_UP nonsense=1     // compiles identically fine

That has two consequences, and they are the reason this folder exists:

  • Good news: you get correct C++ diagnostics with any normal compiler. The pragmas never produce false errors.
  • Bad news: nothing but the HLS compiler can tell a valid pragma from a typo, and even Vitis HLS is often happy to ignore one silently. So a mistyped PIPLINE can quietly cost you the optimisation you asked for.

Hence items 1-2 are for editing comfort, and item 3 is the one that actually catches bugs.


Setup

1. Point Neovim at the highlighting and linter (one line)

Add this to your init.lua:

vim.opt.runtimepath:append(vim.fn.expand('~/Projects/EMBS/Practicals/hlsdev/nvim'))
vim.opt.runtimepath:append(vim.fn.expand('~/Projects/EMBS/Practicals/hlsdev/nvim/after'))

Both lines are needed: the first picks up plugin/, the second picks up after/syntax/. Nothing else to install - no plugin manager involved.

cd ~/Projects/EMBS/Practicals/hlsdev
./fetch-headers.sh --dry-run     # see what is there and how big it is
./fetch-headers.sh               # copy them (asks for your gateway password)

This copies just the header files (a few MB) from /opt/york/cs/net/xilinx_vivado-2025.1-x86_64-1/2025.1/Vitis - not the 60 GB toolchain. clangd and ./sim.sh then use the genuine ap_int.h.

If you skip this, everything still works using the stand-in header in include-stub/. That stub is not the real header - it only implements the handful of ap_uint operations the practicals use, and ./sim.sh prints a warning when it falls back to it.


Daily use

cd ~/Projects/EMBS/Practicals/hlsdev

./sim.sh          # compile + run the C simulation locally (fast)
./lint.sh         # check every #pragma HLS line
./sim.sh practical-4        # any other practical folder
./lint.sh ../practical-3b/game_of_life_practical3b/gameoflife.cpp   # one file

In Neovim:

  • Diagnostics from the pragma linter appear alongside clangd's, automatically on open and on save.
  • :HlsPragmaLint re-runs it on the current buffer.

What the linter catches

$ ./lint.sh
gameoflife.cpp:2:9: error: unknown HLS pragma 'PIPLINE' - did you mean 'PIPELINE'?
gameoflife.cpp:3:9: error: UNROLL: factor must be an integer, got 'two'
gameoflife.cpp:4:9: warning: ARRAY_PARTITION: variable='missing_array' is not declared anywhere in this file
gameoflife.cpp:5:9: error: ARRAY_PARTITION: type='complet' is invalid (expected one of: block, complete, cyclic)
gameoflife.cpp:6:9: error: INTERFACE: 's_axillite' is not a valid mode (expected one of: ... s_axilite ...)
gameoflife.cpp:8:9: error: unknown HLS pragma 'TOTALLY_MADE_UP'
gameoflife.cpp:9:9: error: LOOP_TRIPCOUNT: missing required option 'min='

It checks pragma names, option names, option values, required options, and whether the variable= / port= you named actually exists in the file - with "did you mean" suggestions throughout.


How clangd is wired up

Vitis writes a compile_commands.json next to the sources, but its paths point at /home/userfs/b/... on the server and its flags (-fhls, -hls-syn-headers-dir=, --gcc-toolchain=) are Xilinx-only, so clangd cannot use it.

Rather than overwrite it (Vitis regenerates and hard-codes paths), the file game_of_life_practical3b/.clangd redirects clangd to a local flag set:

CompileFlags:
  CompilationDatabase: ../../hlsdev/clangd

and hlsdev/clangd/compile_flags.txt holds the actual flags:

-xc++
-std=c++14
-Wno-unknown-pragmas
-I../include
-I../include-stub

Relative paths are resolved from the compile_flags.txt directory, so this stays portable - no absolute paths are committed.


Files

File Purpose
sim.sh Compile + run the C simulation locally
lint.sh Run the pragma linter over the practicals
hls_pragma_lint.py The linter itself (also usable in CI)
fetch-headers.sh Copy the real HLS headers down from csteach0
clangd/compile_flags.txt Flags clangd uses for these files
include-stub/ap_int.h Editor-only stand-in for the real ap_int.h
include/ Where fetch-headers.sh puts the real headers (gitignored)
nvim/after/syntax/cpp.vim #pragma HLS highlighting
nvim/plugin/hls_pragma_lint.lua Neovim diagnostics integration

Two caveats

Treesitter will override the highlighting if the cpp parser is installed. Your config runs vim.treesitter.start() on every filetype, and Neovim has no cpp parser installed, so it currently falls back to Vim syntax (where our rules live). If you ever run :TSInstall cpp, the pragma highlighting will stop working. Fix it by not starting treesitter for cpp, e.g.:

vim.api.nvim_create_autocmd('FileType', {
  callback = function()
    if vim.bo.filetype == 'cpp' then return end   -- keep Vim syntax for cpp
    pcall(vim.treesitter.start)
  end,
})

The local simulation is not synthesis. It checks your logic against the testbench, which is the bulk of the work - but it does not tell you about timing, II, resource usage or whether the pragmas did what you wanted. For that you still need a real HLS run on the server. ./lint.sh covers the pragma typos that the compiler would otherwise swallow.