#!/usr/bin/env python3 """ Consumer adoption harness for libakstdlib. Answers one question about a consumer's source tree: how often does it call this library, and how often does it reach past this library to the libc function this library wraps? The ratio is the only external evidence there is about whether the surface that got built is the surface anyone wanted. A wrapper nobody calls is a wrapper that either does not fit or is not discoverable, and both are the library's problem. The number this reports is only worth something if it is reproducible, which is why this is a script and not a paragraph. TODO.md's first consumer figure was recorded without one, and recovering the method afterwards cost more than writing it down would have. WHAT COUNTS * The corpus is every *.c and *.h directly under the given directory. It does not recurse: a consumer's src/ is the thing being measured, not its vendored dependencies, and those are usually a subdirectory. * Comments and string/character literals are stripped before anything is counted, so a function named in prose or inside a format string does not score. This matters more than it sounds -- "strlen" appears in doc comments throughout a codebase that has been thinking about strlen. * A call site is IDENT immediately followed by '(', where IDENT is not preceded by an identifier character. Declarations are not distinguished from calls; a consumer that declares a function named for a libc entry point will over-count by one per declaration, which is visible in --detail. * A library call is any IDENT matching ^aksl_. * A bypass is any IDENT naming a libc function this library wraps. That set is read out of include/akstdlib.h rather than hardcoded, so it grows when the library grows and a recount after a release measures the surface that release actually shipped. * libc functions this library does NOT wrap -- isdigit, exit, qsort -- score on neither side. The question is how often a consumer bypasses an available wrapper, not how much libc it uses. Adding a wrapper for something and having it ignored is a finding; a consumer calling exit() is not. WHAT IT CANNOT TELL YOU One consumer's ratio is evidence, not a plan. A consumer that draws everything from fixed pools will never call the allocator no matter how good the allocator is, and will weight the string wrappers accordingly. Weight the result by what the consumer is, and get a second consumer before treating any ranking as settled. Usage: scripts/consumer_calls.py DIR [options] DIR consumer source directory to measure, e.g. ../akbasic/src --header PATH akstdlib.h to read the wrapped-libc set from (default: include/akstdlib.h beside this script's repo) --detail list the per-function breakdown on both sides --per-file list per-file counts, worst bypass ratio first --baseline A/B compare against a previous count, e.g. --baseline 10/119 --json emit the whole result as JSON instead of text """ import argparse import json import os import re import sys from collections import Counter # Wrapper families that are this library's own constructs rather than a libc # function under a new name. aksl_list_append has no libc counterpart, so # "append" must not become a name a consumer can be scored for bypassing. LIBRARY_ONLY_PREFIXES = ("hashmap", "list", "tree", "strbuf", "version", "strhash") # Library-only names whose first underscore-separated word is shared with a real # libc entry point, so a prefix rule cannot separate them. aksl_realpath wraps # realpath(3) and must score; aksl_realpath_alloc is this library's own. LIBRARY_ONLY_NAMES = frozenset(("freep", "realpath_alloc")) CALL = re.compile(r"(?5}{raw:>6}") return 0 if __name__ == "__main__": sys.exit(main())