diff --git a/fec/src/driver.c b/fec/src/driver.c index 5b721bd..bc78d11 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -3,6 +3,7 @@ #include "resolve.h" #include "lower.h" #include "x86.h" +#include "report.h" #include #include #include @@ -19,6 +20,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--std=dir] [--no-checks]"); + puts(" fec [--report-unsafe|--report-instances] file.fe [--std=dir]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -40,6 +42,7 @@ static void dump_tokens(const char *src, unsigned long n, const char *file, int main(int argc, char **argv) { int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; + int rep_unsafe=0,rep_inst=0; const char *file=0; const char *out_path=0; const char *std_root=0; @@ -63,6 +66,8 @@ int main(int argc, char **argv) /* One target (SPEC 2), so --target= and --model= are gone: a flag that is accepted and does nothing is worse than one that is not accepted at all. */ + else if(strcmp(argv[i],"--report-unsafe")==0) rep_unsafe=1; + else if(strcmp(argv[i],"--report-instances")==0) rep_inst=1; else if(strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} @@ -103,6 +108,11 @@ int main(int argc, char **argv) if(ok){ fe_check_init(&check,&build,&d,pointer_bits,no_checks); if(!fe_check_program(&check)) ok=0; + /* Reports describe the program that was checked, so they come + after checking and instead of code generation. */ + if(rep_unsafe) fe_report_unsafe(&build,stdout); + if(rep_inst) fe_report_instances(&check,stdout); + if(rep_unsafe||rep_inst) { dump_ir=0; emit_asm=0; } if(ok && (dump_ir||emit_asm)){ FeIrModule ir; fe_ir_module_init(&ir); diff --git a/fec/src/report.c b/fec/src/report.c new file mode 100644 index 0000000..90ca99f --- /dev/null +++ b/fec/src/report.c @@ -0,0 +1,120 @@ +#include "report.h" +#include + +/* What `--report-unsafe` and `--report-instances` print. + * + * Both answer a question that is easy to ask and easy to let slide: how much of + * the program is outside what the checker can promise, and how much code the + * generic instances are about to become. A number nobody can produce is not a + * budget, so these are here rather than in a comment somewhere. */ + +typedef struct Counts { + unsigned unsafe_blocks; + unsigned raw_types; + unsigned unchecked_calls; +} Counts; + +/* Does this name end in `_unchecked`? Those are the deliberate holes in the + checked surface, and they are worth counting separately from `unsafe` + because they do not need a block around them. */ +static int is_unchecked(const char *name) +{ + unsigned long n; + unsigned long m = 10UL; /* strlen("_unchecked") */ + if (!name) return 0; + n = (unsigned long)strlen(name); + if (n < m) return 0; + return strcmp(name + (n - m), "_unchecked") == 0; +} + +static void walk(const FeNode *n, Counts *c) +{ + const FeNode *x; + if (!n) return; + if (n->kind == FE_N_UNSAFE) ++c->unsafe_blocks; + if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "*") == 0) + ++c->raw_types; + if (n->kind == FE_N_CALL) { + const char *callee = n->text; + if (!callee && n->a) { + if (n->a->kind == FE_N_IDENT) callee = n->a->text; + else if (n->a->kind == FE_N_MEMBER && n->a->b) + callee = n->a->b->text; + } + if (is_unchecked(callee)) ++c->unchecked_calls; + } + walk(n->a, c); + walk(n->b, c); + walk(n->c, c); + for (x = n->children; x; x = x->next) walk(x, c); +} + +/* The standard library is where the unchecked things are supposed to live, so + it is reported but kept out of the total a program is judged on. */ +static int is_std(const char *unit) +{ + return unit && strncmp(unit, "std.", 4) == 0; +} + +void fe_report_unsafe(const FeBuild *build, FILE *out) +{ + unsigned u; + Counts total; + Counts outside; + total.unsafe_blocks = 0; total.raw_types = 0; total.unchecked_calls = 0; + outside = total; + fprintf(out, "%-20s %8s %8s %10s\n", "unit", "unsafe", "*T", "unchecked"); + for (u = 0; u < build->count; ++u) { + const FeUnit *unit = &build->units[u]; + Counts c; + c.unsafe_blocks = 0; c.raw_types = 0; c.unchecked_calls = 0; + walk(unit->ast.root, &c); + if (!c.unsafe_blocks && !c.raw_types && !c.unchecked_calls) continue; + fprintf(out, "%-20s %8u %8u %10u\n", unit->name, c.unsafe_blocks, + c.raw_types, c.unchecked_calls); + total.unsafe_blocks += c.unsafe_blocks; + total.raw_types += c.raw_types; + total.unchecked_calls += c.unchecked_calls; + if (!is_std(unit->name)) { + outside.unsafe_blocks += c.unsafe_blocks; + outside.raw_types += c.raw_types; + outside.unchecked_calls += c.unchecked_calls; + } + } + fprintf(out, "%-20s %8u %8u %10u\n", "total", total.unsafe_blocks, + total.raw_types, total.unchecked_calls); + fprintf(out, "%-20s %8u %8u %10u\n", "outside std", outside.unsafe_blocks, + outside.raw_types, outside.unchecked_calls); +} + +void fe_report_instances(const FeCheck *c, FILE *out) +{ + unsigned i; + unsigned types = 0; + unsigned methods = 0; + unsigned long bytes = 0; + fprintf(out, "%-52s %6s %8s\n", "instance", "kind", "size"); + for (i = 0; i < c->instance_count; ++i) { + const FeInstance *inst = &c->instances[i]; + unsigned long size = 0; + if (inst->owner) ++methods; + else { + ++types; + /* A struct instance is code only through its methods; what it + costs on its own is the storage one value of it takes. */ + { + const FeType *t; + for (t = c->types.types; t; t = t->next) + if (t->name[0] && !strcmp(t->name, inst->key)) { + size = t->size; + break; + } + } + bytes += size; + } + fprintf(out, "%-52s %6s %8lu\n", inst->key, + inst->owner ? "method" : "type", size); + } + fprintf(out, "\n%u instances: %u types (%lu bytes of storage), %u methods\n", + c->instance_count, types, bytes, methods); +} diff --git a/fec/src/report.h b/fec/src/report.h new file mode 100644 index 0000000..d7546f2 --- /dev/null +++ b/fec/src/report.h @@ -0,0 +1,13 @@ +#ifndef FE_REPORT_H +#define FE_REPORT_H + +#include "check.h" +#include + +/* How much of the build is outside what the checker promises. */ +void fe_report_unsafe(const FeBuild *build, FILE *out); + +/* What the generic instances came to. */ +void fe_report_instances(const FeCheck *c, FILE *out); + +#endif diff --git a/tests/run.py b/tests/run.py index 913c2fd..34b3661 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", "check", "checkexp", "checkstm", "checkgen", "checkcal", "checkpro", - "resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "driver") + "resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "report", "driver") MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$") @@ -132,8 +132,41 @@ def main() -> int: marked = sum(1 for p in cases if expectation(p).line is not None) print(f"\n{len(cases) - len(failed)}/{len(cases)} passed " f"({marked} pin a line and message)") + if not args.select: + leak = unsafe_budget(fec) + if leak: + print(leak) + return 1 return 1 if failed else 0 +# The programs the budget is measured on: the Ferro front end, and the ones +# that lean hardest on the standard library. +BUDGETED = ("exec/lexer/tree.fe", "exec/interns.fe", "exec/arenat.fe", + "exec/maps.fe", "exec/wordfreq.fe") + + +def unsafe_budget(fec: Path) -> str: + """`unsafe` and `*T` belong to std.mem and std.sys. Anywhere else they are + a hole in what the checker promises, so the count outside std has to stay + at zero and a regression has to fail the build rather than be noticed.""" + for rel in BUDGETED: + path = FIXTURES / rel + if not path.is_file(): + return f"budget: {rel} is gone" + done = subprocess.run([str(fec), "--report-unsafe", str(path), + f"--std={ROOT / 'fec'}"], + capture_output=True, text=True) + line = [l for l in done.stdout.splitlines() + if l.startswith("outside std")] + if not line: + return f"budget: no report for {rel}\n{done.stdout}{done.stderr}" + counts = line[0].split()[2:] + if any(c != "0" for c in counts): + return (f"budget: {rel} has unsafe/raw pointers outside std: " + f"{line[0]}") + return "" + + if __name__ == "__main__": raise SystemExit(main())