Add local HLS dev setup: clangd, pragma highlighting + linting, local C-sim

This commit is contained in:
2026-10-24 17:13:18 +01:00
parent e036c194ed
commit 04b7425598
180 changed files with 745644 additions and 0 deletions

178
Practicals/hlsdev/README.md Normal file
View File

@@ -0,0 +1,178 @@
# 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:
```c
#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`:
```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.
### 2. Get the real HLS headers (optional but recommended)
```bash
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
```bash
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:
```yaml
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.:
```lua
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.

View File

@@ -0,0 +1,5 @@
-xc++
-std=c++14
-Wno-unknown-pragmas
-I../include
-I../include-stub

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env bash
#
# Copy the real Vitis HLS headers down from the CS server.
#
# Only header files are copied - a few MB, not the 60 GB toolchain - so clangd
# and ./sim.sh work against the genuine ap_int.h / hls_stream.h instead of the
# stand-in in hlsdev/include-stub.
#
# You will be asked for your gateway password / Duo approval, because the
# server is behind the IT Services SSH gateway.
#
# Usage:
# ./fetch-headers.sh # copy
# ./fetch-headers.sh --dry-run # just report what is there and how big it is
#
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
inc="$here/include"
host="${EMBS_HOST:-csteach0.york.ac.uk}"
vitis="${VITIS_ROOT:-/opt/york/cs/net/xilinx_vivado-2025.1-x86_64-1/2025.1/Vitis}"
sim_inc="$vitis/include"
syn_inc="$vitis/common/technology/autopilot"
echo "host : $host"
echo "sim inc : $sim_inc"
echo "syn inc : $syn_inc"
echo
echo "Asking the server what is there ..."
ssh "$host" "
du -sh '$sim_inc' '$syn_inc' 2>/dev/null || true
echo
echo 'ap_int.h / hls_stream.h found at:'
find '$sim_inc' '$syn_inc' -maxdepth 2 \\( -name ap_int.h -o -name hls_stream.h -o -name ap_fixed.h \\) 2>/dev/null
" || { echo "error: could not reach $host" >&2; exit 1; }
if [ "${1:-}" = "--dry-run" ]; then
echo
echo "--dry-run: nothing copied."
exit 0
fi
mkdir -p "$inc"
echo
echo "Copying headers (watch for the password prompt) ..."
rsync -a --info=progress2 "$host:$sim_inc/" "$inc/"
rsync -a --info=progress2 "$host:$syn_inc/" "$inc/"
echo
echo "Done. $(find "$inc" -name '*.h' | wc -l | tr -d ' ') headers now in $inc"
echo "clangd and ./sim.sh will use these in preference to the stub."

View File

@@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""
hls_pragma_lint.py - check Xilinx Vitis HLS pragmas.
Why this exists
---------------
`#pragma HLS ...` is not C++. Stock clang and gcc do not know these pragmas
and simply ignore them, so a typo like
#pragma HLS PIPLINE II=2
compiles perfectly happily. Vitis HLS may also accept it silently, meaning the
optimisation you asked for just never happens - which is a genuinely nasty way
to lose an afternoon.
This linter knows the pragma names, the options each one takes, and the values
those options allow. It reports problems in the standard
file:line:col: error: message
form, so it works with nvim's errorformat, nvim-lint, efm-langserver, `:make`,
or any CI.
Usage
-----
./hls_pragma_lint.py file.cpp [more.cpp ...]
./hls_pragma_lint.py ../practical-3b/game_of_life_practical3b/gameoflife.cpp
Exit status is 0 when nothing is wrong, 1 when there is at least one error.
"""
import difflib
import re
import sys
# ---------------------------------------------------------------------------
# The pragma reference
# ---------------------------------------------------------------------------
# "FLAG" -> a bare keyword with no value, e.g. `rewind`
# "NUM" -> an integer
# set -> a value that must be one of these
# "ANY" -> anything (identifier, number, ...)
# ---------------------------------------------------------------------------
FLAG, NUM, ANY = "FLAG", "NUM", "ANY"
INTERFACE_MODES = {
"ap_none", "ap_vld", "ap_ack", "ap_hs", "ap_ovld", "ap_fifo",
"ap_memory", "ap_bram", "bram", "ap_ctrl_none", "ap_ctrl_hs",
"ap_ctrl_chain", "m_axi", "s_axilite", "axis", "s_axis", "m_axis",
"ap_stable", "ap_bus", "ap_array", "ap_array_rtl",
}
PRAGMAS = {
"PIPELINE": {
"options": {
"II": NUM, "rewind": FLAG, "off": FLAG, "enable_flush": FLAG,
"style": {"stp", "frp", "flp"},
},
},
"UNROLL": {
"options": {
"factor": NUM, "region": FLAG, "skip_exit_check": FLAG, "off": FLAG,
},
},
"ARRAY_PARTITION": {
"options": {
"variable": ANY, "type": {"block", "cyclic", "complete"},
"factor": NUM, "dim": NUM, "recursive": FLAG, "off": FLAG,
},
"required": ["variable"],
},
"ARRAY_RESHAPE": {
"options": {
"variable": ANY, "type": {"block", "cyclic", "complete"},
"factor": NUM, "dim": NUM, "off": FLAG,
},
"required": ["variable"],
},
"ARRAY_MAP": {
"options": {
"variable": ANY, "instance": ANY,
"mode": {"horizontal", "vertical"}, "offset": NUM, "off": FLAG,
},
"required": ["variable"],
},
"DATA_PACK": {
"options": {
"variable": ANY, "instance": ANY, "offset": NUM, "off": FLAG,
},
},
"AGGREGATE": {
"options": {
"variable": ANY, "compact": {"bit", "byte", "none", "auto"},
"dim": NUM, "off": FLAG,
},
"required": ["variable"],
},
"STREAM": {
"options": {
"variable": ANY, "depth": NUM, "dim": NUM, "off": FLAG,
},
"required": ["variable"],
},
"INTERFACE": {
"positional": INTERFACE_MODES,
"options": {
"mode": INTERFACE_MODES, "port": ANY, "bundle": ANY,
"register": FLAG, "depth": NUM, "offset": ANY, "clock": ANY,
"name": ANY, "num_read_outstanding": NUM,
"num_write_outstanding": NUM, "max_read_burst_length": NUM,
"max_write_burst_length": NUM, "latency": NUM,
"m_axi_offset": ANY, "storage_type": ANY, "storage_impl": ANY,
"sync": FLAG, "max_widen_bitwidth": NUM,
},
"required": ["port"],
},
"DATAFLOW": {
"options": {"off": FLAG, "disable_start_propagation": FLAG},
},
"INLINE": {
"options": {"off": FLAG, "recursive": FLAG, "region": FLAG},
},
"LATENCY": {
"options": {"min": NUM, "max": NUM, "off": FLAG},
},
"DEPENDENCE": {
"options": {
"variable": ANY, "type": {"inter", "intra"},
"direction": {"RAW", "WAR", "WAW"}, "distance": NUM, "off": FLAG,
},
"required": ["variable"],
},
"ALLOCATION": {
"options": {
"limit": NUM, "instances": ANY, "operation": ANY, "off": FLAG,
},
},
"OCCURRENCE": {
"options": {"cycle": NUM, "operation": ANY, "off": FLAG},
},
"LOOP_FLATTEN": {"options": {"off": FLAG}},
"LOOP_MERGE": {"options": {"force": FLAG, "off": FLAG}},
"LOOP_TRIPCOUNT": {
"options": {"min": NUM, "max": NUM, "avg": NUM},
"required": ["min"],
},
"FUNCTION_INSTANTIATE": {
"options": {"variable": ANY}, "required": ["variable"],
},
"RESET": {"options": {"off": FLAG}},
"PROTOCOL": {"positional": {"fixed", "float"}},
"EXPRESSION_BALANCE": {"options": {"off": FLAG}},
"REQUIREMENTS": {"options": {"min": NUM, "max": NUM}},
"DISAGGREGATE": {
"options": {"variable": ANY, "dim": NUM, "type": ANY},
"required": ["variable"],
},
"BIND_OP": {
"options": {
"op": ANY, "impl": ANY, "variable": ANY, "latency": NUM,
},
"required": ["op", "impl"],
},
"BIND_STORAGE": {
"options": {
"type": ANY, "impl": ANY, "variable": ANY, "latency": NUM,
},
"required": ["type", "impl"],
},
"SHARED": {"options": {"off": FLAG}},
}
# Options whose value is a name declared in the code.
NAME_OPTIONS = ("variable", "port", "op")
PRAGMA_RE = re.compile(r"#\s*pragma\s+HLS\b(?P<rest>.*)$")
TOKEN_RE = re.compile(r"(?P<key>[A-Za-z_]\w*)\s*=\s*(?P<val>[^\s=]+)|(?P<bare>[^\s=]+)")
def find_pragma_name(rest):
"""Pull the pragma name off the front of a pragma's argument text."""
m = re.match(r"\s*([A-Za-z_]\w*)", rest)
if not m:
return None, rest
return m.group(1), rest[m.end():]
def parse_tokens(text):
"""Return (options, bare_words) where options is [(key, val, col), ...]."""
options, bare = [], []
for m in TOKEN_RE.finditer(text):
if m.group("key"):
options.append((m.group("key"), m.group("val"), m.start("key")))
else:
bare.append((m.group("bare"), m.start("bare")))
return options, bare
def lint_file(path):
"""Return a list of (line, col, severity, message)."""
out = []
try:
with open(path, "r", errors="replace") as fh:
lines = fh.readlines()
except OSError as exc:
return [(1, 1, "error", "cannot read file: %s" % exc)]
whole_file = "".join(lines)
for lineno, line in enumerate(lines, start=1):
m = PRAGMA_RE.search(line)
if not m:
continue
rest = m.group("rest")
name, args = find_pragma_name(rest)
col = m.start() + m.group(0).index("HLS") + 1
if name is None:
out.append((lineno, col, "error",
"HLS pragma has no name (expected e.g. PIPELINE, UNROLL)"))
continue
if name not in PRAGMAS:
hint = difflib.get_close_matches(name, PRAGMAS.keys(), n=1, cutoff=0.6)
msg = "unknown HLS pragma '%s'" % name
if hint:
msg += " - did you mean '%s'?" % hint[0]
else:
msg += " (see the Vitis HLS pragma reference)"
out.append((lineno, col, "error", msg))
continue
spec = PRAGMAS[name]
known = spec.get("options", {})
positional = spec.get("positional")
options, bare = parse_tokens(args)
seen = {k for k, _, _ in options}
# Bare words: either a positional value or a flag.
for word, off in bare:
if word in known and known[word] is FLAG:
seen.add(word)
continue
if positional and word in positional:
continue
if positional and word not in positional:
out.append((lineno, col, "error",
"%s: '%s' is not a valid mode (expected one of: %s)"
% (name, word, ", ".join(sorted(positional)))))
continue
if word in known:
out.append((lineno, col, "error",
"%s: '%s' needs a value, e.g. %s=..." % (name, word, word)))
continue
hint = difflib.get_close_matches(word, list(known) + list(positional or ()),
n=1, cutoff=0.6)
msg = "%s: unknown option '%s'" % (name, word)
if hint:
msg += " - did you mean '%s'?" % hint[0]
out.append((lineno, col, "error", msg))
# key=value options
for key, val, off in options:
if key not in known:
hint = difflib.get_close_matches(key, known.keys(), n=1, cutoff=0.6)
msg = "%s: unknown option '%s'" % (name, key)
if hint:
msg += " - did you mean '%s'?" % hint[0]
out.append((lineno, col, "error", msg))
continue
expect = known[key]
if expect is FLAG:
out.append((lineno, col, "warning",
"%s: '%s' is a flag and takes no value" % (name, key)))
elif expect is NUM:
if not re.fullmatch(r"\d+", val):
out.append((lineno, col, "error",
"%s: %s must be an integer, got '%s'"
% (name, key, val)))
elif isinstance(expect, set):
if val not in expect:
out.append((lineno, col, "error",
"%s: %s='%s' is invalid (expected one of: %s)"
% (name, key, val, ", ".join(sorted(expect)))))
if key in NAME_OPTIONS and val != "return":
# Count occurrences across the file. A real object is declared
# at least once and named in at least one pragma, so a single
# hit means the only mention is this pragma itself.
hits = len(re.findall(r"\b%s\b" % re.escape(val), whole_file))
if hits <= 1:
out.append((lineno, col, "warning",
"%s: %s='%s' is not declared anywhere in this file"
% (name, key, val)))
# Required options
for req in spec.get("required", []):
if req not in seen and not (positional and req in positional):
out.append((lineno, col, "error",
"%s: missing required option '%s='" % (name, req)))
return out
def main(argv):
if len(argv) < 2:
print(__doc__.strip(), file=sys.stderr)
return 2
errors = 0
for path in argv[1:]:
for line, col, sev, msg in lint_file(path):
print("%s:%d:%d: %s: %s" % (path, line, col, sev, msg))
if sev == "error":
errors += 1
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@@ -0,0 +1,75 @@
#ifndef HLSDEV_AP_INT_STUB_H
#define HLSDEV_AP_INT_STUB_H
// ===========================================================================
// EDITOR-ONLY STAND-IN for Xilinx's <ap_int.h>
// ---------------------------------------------------------------------------
// This is NOT the real header. It exists so clangd can resolve ap_uint<N>
// (and so a simple C-simulation can be built) on a machine with no Xilinx
// tools installed.
//
// It implements only what the EMBS practicals use: unsigned fixed-width
// values, bit indexing, += / -=, and conversion to/from integers.
// It supports widths 1..64 only, and has none of the real header's
// arbitrary-precision, signed, overflow or simulation semantics.
//
// Run ./fetch-headers.sh to download the REAL headers from csteach0.
// They take precedence over this file when present.
// ===========================================================================
#include <cstdint>
template <int W>
class ap_uint {
static_assert(W > 0 && W <= 64, "hlsdev stub ap_uint supports widths 1..64");
public:
unsigned long long v;
static constexpr unsigned long long width_mask() {
return W >= 64 ? ~0ULL : ((1ULL << W) - 1ULL);
}
ap_uint() : v(0) {}
ap_uint(int x) : v((unsigned long long)x & width_mask()) {}
ap_uint(unsigned int x) : v((unsigned long long)x & width_mask()) {}
ap_uint(unsigned long long x) : v(x & width_mask()) {}
// Cross-width conversion, so e.g. ap_uint<4> += ap_uint<1> works.
// The implicit copy constructor is still preferred for the same width.
template <int W2>
ap_uint(const ap_uint<W2>& o) : v((unsigned long long)o & width_mask()) {}
operator unsigned long long() const { return v & width_mask(); }
ap_uint& operator=(unsigned long long x) { v = x & width_mask(); return *this; }
ap_uint& operator+=(const ap_uint& o) { v = (v + o.v) & width_mask(); return *this; }
ap_uint& operator-=(const ap_uint& o) { v = (v - o.v) & width_mask(); return *this; }
// Writing a single bit needs a proxy object, just like the real ap_uint.
class bit_ref {
ap_uint& parent;
int bit;
public:
bit_ref(ap_uint& p, int b) : parent(p), bit(b) {}
bit_ref& operator=(bool x) {
if (x) parent.v |= (1ULL << bit);
else parent.v &= ~(1ULL << bit);
return *this;
}
operator bool() const { return ((parent.v >> bit) & 1ULL) != 0; }
// Convert straight to any ap_uint<N> in a single user-defined
// conversion, so things like acc += grid[r][c]; resolve cleanly.
template <int W2>
operator ap_uint<W2>() const { return ap_uint<W2>((parent.v >> bit) & 1ULL); }
};
bit_ref operator[](int b) { return bit_ref(*this, b); }
bool operator[](int b) const { return ((v >> b) & 1ULL) != 0; }
};
#endif // HLSDEV_AP_INT_STUB_H

39
Practicals/hlsdev/lint.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
#
# Run the HLS pragma linter over the practicals.
#
# Usage:
# ./lint.sh # every source under Practicals/
# ./lint.sh path/to/file.cpp [more.cpp ...] # just these
#
# Exits non-zero if anything is wrong, so it also works as a pre-commit or CI
# check. For inline diagnostics inside nvim, see nvim/plugin/hls_pragma_lint.lua
#
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ "$#" -gt 0 ]; then
exec python3 "$here/hls_pragma_lint.py" "$@"
fi
# Resolve the parent properly: find returns the path as given, and
# ".../hlsdev/.." would contain "/hlsdev/" and defeat the exclusions below.
root="$(cd "$here/.." && pwd)"
# Every hand-written source, skipping generated HLS build output and our own
# tooling. Paths in the practicals never contain spaces (Vitis requires it).
files="$(find "$root" -type f \( \
-name '*.cpp' -o -name '*.cc' -o -name '*.cxx' -o -name '*.c' \
-o -name '*.h' -o -name '*.hpp' \) \
-not -path '*/hls/*' -not -path '*/hlsdev/*' | sort)"
if [ -z "$files" ]; then
echo "no source files found" >&2
exit 0
fi
echo "$files" | sed "s|^$root/| |"
echo
# shellcheck disable=SC2086
exec python3 "$here/hls_pragma_lint.py" $files

View File

@@ -0,0 +1,43 @@
" ===========================================================================
" HLS pragma highlighting for Vitis HLS (EMBS)
" ---------------------------------------------------------------------------
" Vim's stock C/C++ syntax files treat the whole "#pragma HLS ..." line as one
" anonymous preprocessor directive, so the pragma name and its options are
" invisible. These rules give them their own highlight groups.
"
" Loaded automatically when this directory is on 'runtimepath' (see the
" hlsdev README). Safe to source twice.
" ===========================================================================
if !exists('b:current_syntax')
finish
endif
if b:current_syntax !=# 'cpp' && b:current_syntax !=# 'c'
finish
endif
" The pragma line as a whole, so options inside it can be picked out.
syntax region hlsPragma
\ start=/#\s*pragma\s\+HLS\>/
\ end="$"
\ keepend
\ contains=hlsPragmaName,hlsPragmaOption,hlsPragmaValue,hlsPragmaMode
" The pragma name: PIPELINE, UNROLL, ARRAY_PARTITION, INTERFACE, ...
syntax match hlsPragmaName /\<HLS\>\s\+\zs[A-Z][A-Z_]*/ contained
" Option keys: variable=, II=, type=, port=, bundle=
syntax match hlsPragmaOption /\<\w\+\ze\s*=/ contained
" Option values: complete, 2, in_words, AXILiteS
syntax match hlsPragmaValue /=\s*\zs[^ ]\+/ contained
" INTERFACE modes are positional, so they have no "mode=" in front of them:
" #pragma HLS INTERFACE s_axilite port=return bundle=ctrl
syntax match hlsPragmaMode /\<\%(ap_none\|ap_vld\|ap_ack\|ap_hs\|ap_ovld\|ap_fifo\|ap_memory\|ap_bram\|bram\|ap_ctrl_none\|ap_ctrl_hs\|ap_ctrl_chain\|m_axi\|s_axilite\|axis\|s_axis\|m_axis\|ap_stable\|ap_bus\)\>/ contained
highlight default link hlsPragma PreProc
highlight default link hlsPragmaName Statement
highlight default link hlsPragmaOption Identifier
highlight default link hlsPragmaValue Constant
highlight default link hlsPragmaMode Constant

View File

@@ -0,0 +1,92 @@
-- ===========================================================================
-- HLS pragma diagnostics for Neovim (EMBS)
-- ---------------------------------------------------------------------------
-- Runs hls_pragma_lint.py over C/C++ buffers and reports the results through
-- vim.diagnostic, so they show up exactly like clangd's errors.
--
-- No plugin manager involved: this file lives in the hlsdev nvim runtimepath
-- entry, and Neovim sources plugin/*.lua automatically.
--
-- Commands:
-- :HlsPragmaLint re-run on the current buffer
-- :lua vim.diagnostic.reset(...) (see :h vim.diagnostic)
-- ===========================================================================
if vim.g.loaded_hls_pragma_lint then
return
end
vim.g.loaded_hls_pragma_lint = true
local ns = vim.api.nvim_create_namespace('hls_pragma_lint')
-- Locate hls_pragma_lint.py next to this runtimepath entry.
local plugin_dir = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':p:h')
local linter = vim.fn.fnamemodify(plugin_dir, ':h:h') .. '/hls_pragma_lint.py'
local function python()
if vim.fn.executable('python3') == 1 then
return 'python3'
end
return nil
end
local SEVERITY = {
error = vim.diagnostic.severity.ERROR,
warning = vim.diagnostic.severity.WARN,
info = vim.diagnostic.severity.INFO,
}
local function parse(output, bufnr)
local diags = {}
for line in vim.gsplit(output, '\n', { plain = true }) do
local _, l, c, sev, msg = line:match('^(.-):(%d+):(%d+): (%a+): (.*)$')
if l then
table.insert(diags, {
bufnr = bufnr,
lnum = tonumber(l) - 1,
col = tonumber(c) - 1,
severity = SEVERITY[sev] or vim.diagnostic.severity.INFO,
message = msg,
source = 'hls-pragma',
})
end
end
return diags
end
local function lint(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
if not vim.api.nvim_buf_is_valid(bufnr) then
return
end
local ft = vim.bo[bufnr].filetype
if ft ~= 'c' and ft ~= 'cpp' then
return
end
if vim.fn.filereadable(linter) ~= 1 then
return
end
local py = python()
if not py then
return
end
local path = vim.api.nvim_buf_get_name(bufnr)
if path == '' then
return
end
local out = vim.fn.system({ py, linter, path })
vim.diagnostic.set(ns, bufnr, parse(out, bufnr))
end
vim.api.nvim_create_autocmd({ 'BufWritePost', 'BufReadPost' }, {
group = vim.api.nvim_create_augroup('HlsPragmaLint', { clear = true }),
pattern = { '*.c', '*.cpp', '*.cc', '*.h', '*.hpp' },
callback = function(ev)
lint(ev.buf)
end,
})
vim.api.nvim_create_user_command('HlsPragmaLint', function()
lint(0)
end, { desc = 'Check #pragma HLS lines in this buffer' })

75
Practicals/hlsdev/sim.sh Executable file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bash
#
# Build and run the Vitis HLS C-simulation for a practical, locally.
#
# This is a plain C++ compile of the testbench + DUT, which is what "Run C
# Simulation" does on the server (minus the HLS compiler's extra checking).
# It is the fast way to know your logic is right before spending time on the
# remote machine.
#
# Usage:
# ./sim.sh # practical-3b
# ./sim.sh practical-4 # any other practical directory
#
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
practical="${1:-practical-3b}"
# Find the HLS component: the directory holding hls_config.cfg.
cfg="$(find "$here/../$practical" -name hls_config.cfg -not -path '*/hls/*' 2>/dev/null | head -1)"
if [ -z "$cfg" ]; then
echo "error: no hls_config.cfg found under $here/../$practical" >&2
exit 1
fi
proj="$(cd "$(dirname "$cfg")" && pwd)"
# hls_config.cfg names the sources, so we never have to guess.
tb_name="$(grep -E '^tb\.file=' "$cfg" | head -1 | cut -d= -f2- | tr -d '\r')"
dut_name="$(grep -E '^syn\.file=' "$cfg" | grep -E '\.(cpp|cc|cxx|c)$' | head -1 | cut -d= -f2- | tr -d '\r')"
if [ -z "$tb_name" ] || [ -z "$dut_name" ]; then
echo "error: could not read tb.file / syn.file from $cfg" >&2
exit 1
fi
# Prefer the real Xilinx headers; fall back to the stub so this still works
# before ./fetch-headers.sh has been run.
if [ -f "$here/include/ap_int.h" ]; then
inc="$here/include"
inc_extra="$here/include"
else
inc="$here/include-stub"
inc_extra="$here/include-stub"
echo "=============================================================="
echo "NOTE: using the FALLBACK stub headers in hlsdev/include-stub."
echo " They are good enough for the Game of Life practical, but"
echo " they are NOT the real Xilinx headers. Run ./fetch-headers.sh"
echo " for a simulation you can fully trust."
echo "=============================================================="
echo
fi
CXX="${CXX:-clang++}"
out="$here/build"
mkdir -p "$out"
dut="$proj/$dut_name"
tb="$proj/$tb_name"
for f in "$dut" "$tb"; do
[ -f "$f" ] || { echo "error: missing source $f" >&2; exit 1; }
done
echo "component : $proj"
echo "testbench : $tb_name"
echo "dut : $dut_name"
echo "compiling with $CXX ..."
"$CXX" -std=c++14 -O2 -Wall -Wextra -Wno-unknown-pragmas -Wno-unused-label \
-I"$inc" -I"$inc_extra" -I"$proj" \
"$tb" "$dut" -o "$out/csim"
echo "running ..."
echo "------------------------------------------------------------------"
"$out/csim"
echo "------------------------------------------------------------------"