summaryrefslogtreecommitdiff
path: root/build-aux
diff options
context:
space:
mode:
Diffstat (limited to 'build-aux')
-rwxr-xr-xbuild-aux/embed-sources.h.gen2
-rw-r--r--build-aux/requirements.txt9
-rwxr-xr-xbuild-aux/stack.c.gen274
-rw-r--r--build-aux/stack.py184
-rwxr-xr-xbuild-aux/stack.sh2
5 files changed, 279 insertions, 192 deletions
diff --git a/build-aux/embed-sources.h.gen b/build-aux/embed-sources.h.gen
index 0ba6457..d473094 100755
--- a/build-aux/embed-sources.h.gen
+++ b/build-aux/embed-sources.h.gen
@@ -1,5 +1,5 @@
#!/bin/sh
-# embed-sources.h.gen - Generate C definitions for GNU `ld -r -b binary` files
+# build-aux/embed-sources.h.gen - Generate C definitions for GNU `ld -r -b binary` files
#
# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/build-aux/requirements.txt b/build-aux/requirements.txt
new file mode 100644
index 0000000..03c0b3d
--- /dev/null
+++ b/build-aux/requirements.txt
@@ -0,0 +1,9 @@
+# build-aux/requirements.txt - List of Python dev requirements
+#
+# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+mypy
+types-gdb
+black
+isort
diff --git a/build-aux/stack.c.gen b/build-aux/stack.c.gen
index 9325791..06612ac 100755
--- a/build-aux/stack.c.gen
+++ b/build-aux/stack.c.gen
@@ -1,9 +1,273 @@
-#!/usr/bin/env bash
-# stack.c.gen - Analyze stack sizes for compiled objects
+#!/usr/bin/env python3
+# build-aux/stack.c.gen - Analyze stack sizes for compiled objects
#
# Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
-for obj in "$@"; do
- echo "// $obj"
-done
+import re
+import sys
+import typing
+
+################################################################################
+#
+# Parse the "VCG" language
+#
+# https://www.rw.cdl.uni-saarland.de/people/sander/private/html/gsvcg1.html
+#
+# The formal syntax is found at
+# ftp://ftp.cs.uni-sb.de/pub/graphics/vcg/vcg.tgz `doc/grammar.txt`.
+
+
+class VCGElem:
+ typ: str
+ lineno: int
+ attrs: dict[str, str]
+
+
+def parse_vcg(reader: typing.TextIO) -> typing.Iterator[VCGElem]:
+ re_beg = re.compile(r"(edge|node):\s*\{\s*")
+ _re_tok = r"[a-zA-Z_][a-zA-Z0-9_]*"
+ _re_str = r'"(?:[^\"]|\\.)*"'
+ re_attr = re.compile(
+ "(" + _re_tok + r")\s*:\s*(" + _re_tok + "|" + _re_str + r")\s*"
+ )
+ re_end = re.compile(r"\}\s*$")
+ re_skip = re.compile(r"(graph:\s*\{\s*title\s*:\s*" + _re_str + r"\s*|\})\s*")
+ re_esc = re.compile(r"\\.")
+
+ for lineno, line in enumerate(reader):
+ pos = 0
+
+ def _raise(msg: str) -> typing.NoReturn:
+ nonlocal lineno
+ nonlocal line
+ nonlocal pos
+ e = SyntaxError(msg)
+ e.lineno = lineno
+ e.offset = pos
+ e.text = line
+ raise e
+
+ if re_skip.fullmatch(line):
+ continue
+
+ elem = VCGElem()
+ elem.lineno = lineno
+
+ m = re_beg.match(line, pos=pos)
+ if not m:
+ _raise("does not look like a VCG line")
+ elem.typ = m.group(1)
+ pos = m.end()
+
+ elem.attrs = {}
+ while True:
+ if re_end.match(line, pos=pos):
+ break
+ m = re_attr.match(line, pos=pos)
+ if not m:
+ _raise("unexpected character")
+ k = m.group(1)
+ v = m.group(2)
+ if k in elem.attrs:
+ _raise(f"duplicate key: {repr(k)}")
+ if v.startswith('"'):
+
+ def unesc(esc: re.Match[str]) -> str:
+ match esc.group(0)[1:]:
+ case "n":
+ return "\n"
+ case '"':
+ return '"'
+ case "\\":
+ return "\\"
+ case _:
+ _raise(f"invalid escape code {repr(esc.group(0))}")
+
+ v = re_esc.sub(unesc, v[1:-1])
+ elem.attrs[k] = v
+ pos = m.end()
+
+ yield elem
+
+
+################################################################################
+# Main application
+
+
+class Node:
+ # from .title (`static` and `__weak` functions are prefixed with
+ # the compilation unit .c file. For static functions that's fine,
+ # but we'll have to handle it specially for __weak.).
+ funcname: str
+ # .label is "{funcname}\n{location}\n{nstatic} bytes (static}\n{ndynamic} dynamic objects"
+ location: str
+ nstatic: int
+ ndynamic: int
+
+ # edges with .sourcename set to this node
+ calls: set[str]
+
+
+re_location = re.compile(r"(?P<filename>.+):(?P<row>[0-9]+):(?P<col>[0-9]+)")
+
+
+def read_source(location: str) -> str:
+ m = re_location.fullmatch(location)
+ if not m:
+ raise ValueError(f"unexpected label value {repr(location)}")
+ filename = m.group("filename")
+ row = int(m.group("row")) - 1
+ col = int(m.group("col")) - 1
+ with open(m.group("filename"), "r") as fh:
+ return fh.readlines()[row][col:].rstrip()
+
+
+def main(ci_fnames: list[str]) -> None:
+ re_node_label = re.compile(
+ r"(?P<funcname>[^\n]+)\n"
+ + r"(?P<location>[^\n]+:[0-9]+:[0-9]+)\n"
+ + r"(?P<nstatic>[0-9]+) bytes \(static\)\n"
+ + r"(?P<ndynamic>[0-9]+) dynamic objects",
+ flags=re.MULTILINE,
+ )
+ re_call_vcall = re.compile(r"VCALL\((?P<obj>[^,]+), (?P<meth>[^,)]+)[,)].*")
+ re_call_other = re.compile(r"(?P<func>[^(]+)\(.*")
+
+ graph: dict[str, Node] = dict()
+ qualified: dict[str, set[str]] = dict()
+
+ def handle_elem(elem: VCGElem) -> None:
+ match elem.typ:
+ case "node":
+ node = Node()
+ node.calls = set()
+ skip = False
+ for k, v in elem.attrs.items():
+ match k:
+ case "title":
+ node.funcname = v
+ case "label":
+ if elem.attrs.get("shape", "") != "ellipse":
+ m = re_node_label.fullmatch(v)
+ if not m:
+ raise ValueError(
+ f"unexpected label value {repr(v)}"
+ )
+ node.location = m.group("location")
+ node.nstatic = int(m.group("nstatic"))
+ node.ndynamic = int(m.group("ndynamic"))
+ case "shape":
+ if v != "ellipse":
+ raise ValueError(f"unexpected shape value {repr(v)}")
+ skip = True
+ case _:
+ raise ValueError(f"unknown edge key {repr(k)}")
+ if not skip:
+ if node.funcname in graph:
+ raise ValueError(f"duplicate node {repr(node.funcname)}")
+ graph[node.funcname] = node
+ if ":" in node.funcname:
+ _, shortname = node.funcname.rsplit(":", 1)
+ if shortname not in qualified:
+ qualified[shortname] = set()
+ qualified[shortname].add(node.funcname)
+ case "edge":
+ caller: str | None = None
+ callee: str | None = None
+ for k, v in elem.attrs.items():
+ match k:
+ case "sourcename":
+ caller = v
+ case "targetname":
+ callee = v
+ case "label":
+ pass
+ case _:
+ raise ValueError(f"unknown edge key {repr(k)}")
+ if caller is None or callee is None:
+ raise ValueError(f"incomplete edge: {repr(elem.attrs)}")
+ if caller not in graph:
+ raise ValueError(f"unknown caller: {caller}")
+ if callee == "__indirect_call":
+ callstr = read_source(elem.attrs.get("label", ""))
+ if m := re_call_vcall.fullmatch(callstr):
+ callee += f":{m.group('obj')}->vtable->{m.group('meth')}"
+ elif m := re_call_other.fullmatch(callstr):
+ callee += f":{m.group('func')}"
+ else:
+ callee += f':{elem.attrs.get("label", "")}'
+ graph[caller].calls.add(callee)
+ case _:
+ raise ValueError(f"unknown elem type {repr(elem.typ)}")
+
+ for ci_fname in ci_fnames:
+ with open(ci_fname, "r") as fh:
+ for elem in parse_vcg(fh):
+ handle_elem(elem)
+
+ missing: set[str] = set()
+ cycles: set[str] = set()
+
+ print("/*")
+
+ dbg = False
+
+ def nstatic(funcname: str, chain: list[str] = []) -> int:
+ nonlocal dbg
+ if funcname not in graph:
+ if f"__wrap_{funcname}" in graph:
+ # Handle `ld --wrap` functions
+ funcname = f"__wrap_{funcname}"
+ elif funcname in qualified and len(qualified[funcname]) == 1:
+ # Handle `__weak` functions
+ funcname = sorted(qualified[funcname])[0]
+ else:
+ missing.add(funcname)
+ return 0
+ if funcname in chain:
+ if "__assert_msg_fail" in chain:
+ if funcname == "__wrap_printf":
+ return 0
+ pass
+ else:
+ cycles.add(f"{chain[chain.index(funcname):] + [funcname]}")
+ return 9999999
+ node = graph[funcname]
+ if dbg:
+ print(f"//dbg: {funcname}\t{node.nstatic}")
+ return node.nstatic + max(
+ [0, *[nstatic(call, chain + [funcname]) for call in node.calls]]
+ )
+
+ def thread_filter(name: str) -> bool:
+ return name.endswith("_cr") or name == "main"
+
+ namelen = max(len(name) for name in graph if thread_filter(name))
+ numlen = max(len(str(nstatic(name))) for name in graph if name.endswith("_cr"))
+ print(("=" * namelen) + " " + "=" * numlen)
+
+ for funcname in graph:
+ if thread_filter(funcname):
+ # dbg = "dhcp" in funcname
+ print(f"{funcname.ljust(namelen)} {str(nstatic(funcname)).rjust(numlen)}")
+
+ print(("=" * namelen) + " " + "=" * numlen)
+
+ for funcname in sorted(missing):
+ print(f"warning: missing: {funcname}")
+ for cycle in sorted(cycles):
+ print(f"warning: cycle: {cycle}")
+
+ print("*/")
+
+
+if __name__ == "__main__":
+ re_suffix = re.compile(r"\.c\.o(bj)?$")
+ main(
+ [
+ re_suffix.sub(".c.ci", fname)
+ for fname in sys.argv[1:]
+ if re_suffix.search(fname)
+ ]
+ )
diff --git a/build-aux/stack.py b/build-aux/stack.py
deleted file mode 100644
index c1e36d3..0000000
--- a/build-aux/stack.py
+++ /dev/null
@@ -1,184 +0,0 @@
-#!/usr/bin/env python3
-import re
-import sys
-import typing
-
-# Parse the "VCG" language
-#
-# https://www.rw.cdl.uni-saarland.de/people/sander/private/html/gsvcg1.html
-#
-# The formal syntax is found at
-# ftp://ftp.cs.uni-sb.de/pub/graphics/vcg/vcg.tgz `doc/grammar.txt`.
-
-
-class VCGElem:
- typ: str
- lineno: int
- attrs: dict[str, str]
-
-
-def parse_vcg(reader: typing.TextIO) -> typing.Iterator[VCGElem]:
- re_beg = re.compile(r"(edge|node):\s*\{\s*")
- _re_tok = r"[a-zA-Z_][a-zA-Z0-9_]*"
- _re_str = r'"(?:[^\"]|\\.)*"'
- re_attr = re.compile(
- "(" + _re_tok + r")\s*:\s*(" + _re_tok + "|" + _re_str + r")\s*"
- )
- re_end = re.compile(r"\}\s*$")
- re_skip = re.compile(r"(graph:\s*\{\s*title\s*:\s*" + _re_str + r"\s*|\})\s*")
- re_esc = re.compile(r"\\.")
-
- for lineno, line in enumerate(reader):
- pos = 0
-
- def _raise(msg: str) -> typing.NoReturn:
- nonlocal lineno
- nonlocal line
- nonlocal pos
- e = SyntaxError(msg)
- e.lineno = lineno
- e.offset = pos
- e.text = line
- raise e
-
- if re_skip.fullmatch(line):
- continue
-
- elem = VCGElem()
- elem.lineno = lineno
-
- m = re_beg.match(line, pos=pos)
- if not m:
- _raise("does not look like a VCG line")
- elem.typ = m.group(1)
- pos = m.end()
-
- elem.attrs = {}
- while True:
- if re_end.match(line, pos=pos):
- break
- m = re_attr.match(line, pos=pos)
- if not m:
- _raise("unexpected character")
- k = m.group(1)
- v = m.group(2)
- if k in elem.attrs:
- _raise(f"duplicate key: {repr(k)}")
- if v.startswith('"'):
-
- def unesc(esc: re.Match[str]) -> str:
- match esc.group(0)[1:]:
- case "n":
- return "\n"
- case '"':
- return '"'
- case "\\":
- return "\\"
- case _:
- _raise(f"invalid escape code {repr(esc.group(0))}")
-
- v = re_esc.sub(unesc, v[1:-1])
- elem.attrs[k] = v
- pos = m.end()
-
- yield elem
-
-
-class Node:
- # from .title (`static` functions are prefixed with the
- # compilation unit .c file, which is fine, we'll just leave it).
- funcname: str
- # .label is "{funcname}\n{location}\n{nstatic} bytes (static}\n{ndynamic} dynamic objects"
- location: str
- nstatic: int
- ndynamic: int
-
- # edges with .sourcename set to this node
- calls: set[str]
-
-def main() -> None:
- re_label = re.compile(
- r"(?P<funcname>[^\n]+)\n"
- + r"(?P<location>[^\n]+:[0-9]+:[0-9]+)\n"
- + r"(?P<nstatic>[0-9]+) bytes \(static\)\n"
- + r"(?P<ndynamic>[0-9]+) dynamic objects",
- flags=re.MULTILINE,
- )
-
- graph: dict[str, Node] = dict()
-
- for elem in parse_vcg(sys.stdin):
- match elem.typ:
- case "node":
- node = Node()
- node.calls = set()
- skip = False
- for k, v in elem.attrs.items():
- match k:
- case "title":
- node.funcname = v
- case "label":
- if elem.attrs.get("shape", "") != "ellipse":
- m = re_label.fullmatch(v)
- if not m:
- raise ValueError(
- f"unexpected label value {repr(v)}"
- )
- node.location = m.group("location")
- node.nstatic = int(m.group("nstatic"))
- node.ndynamic = int(m.group("ndynamic"))
- case "shape":
- if v != "ellipse":
- raise ValueError(f"unexpected shape value {repr(v)}")
- skip = True
- case _:
- raise ValueError(f"unknown edge key {repr(k)}")
- if not skip:
- if node.funcname in graph:
- raise ValueError(f"duplicate node {repr(node.funcname)}")
- graph[node.funcname] = node
- case "edge":
- caller: str | None = None
- callee: str | None = None
- for k, v in elem.attrs.items():
- match k:
- case "sourcename":
- caller = v
- case "targetname":
- callee = v
- case "label":
- pass
- case _:
- raise ValueError(f"unknown edge key {repr(k)}")
- if caller is None or callee is None:
- raise ValueError(f"incomplete edge: {repr(elem.attrs)}")
- if caller not in graph:
- raise ValueError(f"unknown caller: {caller}")
- graph[caller].calls.add(callee)
- case _:
- raise ValueError(f"unknown elem type {repr(elem.typ)}")
-
- # x
-
- missing: set[str] = set()
- def nstatic(funcname: str) -> int:
- if funcname not in graph:
- missing.add(funcname)
- return 0
- node = graph[funcname]
- return node.nstatic + max([0, *[nstatic(call) for call in node.calls]])
-
- namelen = max(len(name) for name in graph if name.endswith("_cr"))
- print(("="*namelen)+" =======")
-
- for funcname in graph:
- if funcname.endswith("_cr"):
- print(f"{funcname}\t{nstatic(funcname)}")
-
- print(("="*namelen)+" =======")
-
- for funcname in sorted(missing):
- print(f"{funcname}\tmissing")
-
-if __name__ == "__main__":
- main()
diff --git a/build-aux/stack.sh b/build-aux/stack.sh
deleted file mode 100755
index 4a8a12e..0000000
--- a/build-aux/stack.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-make >&2 && find build -name '*.ci' -exec cat -- {} + | python ./stack.py