Files
EMBS/Practicals/hlsdev/hls_pragma_lint.py

323 lines
11 KiB
Python
Executable File

#!/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))