Add local HLS dev setup: clangd, pragma highlighting + linting, local C-sim
This commit is contained in:
178
Practicals/hlsdev/README.md
Normal file
178
Practicals/hlsdev/README.md
Normal 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.
|
||||
5
Practicals/hlsdev/clangd/compile_flags.txt
Normal file
5
Practicals/hlsdev/clangd/compile_flags.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
-xc++
|
||||
-std=c++14
|
||||
-Wno-unknown-pragmas
|
||||
-I../include
|
||||
-I../include-stub
|
||||
53
Practicals/hlsdev/fetch-headers.sh
Executable file
53
Practicals/hlsdev/fetch-headers.sh
Executable 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."
|
||||
322
Practicals/hlsdev/hls_pragma_lint.py
Executable file
322
Practicals/hlsdev/hls_pragma_lint.py
Executable 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))
|
||||
75
Practicals/hlsdev/include-stub/ap_int.h
Normal file
75
Practicals/hlsdev/include-stub/ap_int.h
Normal 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
39
Practicals/hlsdev/lint.sh
Executable 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
|
||||
43
Practicals/hlsdev/nvim/after/syntax/cpp.vim
Normal file
43
Practicals/hlsdev/nvim/after/syntax/cpp.vim
Normal 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
|
||||
92
Practicals/hlsdev/nvim/plugin/hls_pragma_lint.lua
Normal file
92
Practicals/hlsdev/nvim/plugin/hls_pragma_lint.lua
Normal 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
75
Practicals/hlsdev/sim.sh
Executable 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 "------------------------------------------------------------------"
|
||||
Reference in New Issue
Block a user