GOAL P4: --report-unsafe 와 --report-instances, 그리고 예산을 CI 에
'ffec 에서 unsafe 가 몇 군데인가' 는 좋은 지표인데 세는 방법이 없으면 지표가 아니다. 이제 센다. unit unsafe *T unchecked std.io 6 1 0 std.sys 10 12 0 total 16 13 0 outside std 0 0 0 Ferro 렉서와 파서, interner, 아레나, 맵을 쓰는 프로그램 전부가 std 바깥에서 0 이다. 목표치를 이미 지키고 있었던 셈인데, 그것을 아무도 확인할 수 없었다. run.py 가 그 숫자를 검사한다. std.mem 과 std.sys 밖의 unsafe 와 *T 는 검사기가 약속한 것에 뚫린 구멍이므로 0 을 유지해야 하고, 늘어나면 알아채는 것이 아니라 빌드가 실패해야 한다. unsafe 블록 하나를 넣어 실패하는 것까지 확인했다. --report-instances 는 제네릭이 무엇이 됐는지 센다. interns.fe 는 20 인스턴스, 타입 4 개에 저장소 80 바이트, 메서드 16 개다. 245/245, 38/38.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
#include "resolve.h"
|
#include "resolve.h"
|
||||||
#include "lower.h"
|
#include "lower.h"
|
||||||
#include "x86.h"
|
#include "x86.h"
|
||||||
|
#include "report.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -19,6 +20,7 @@ static char *read_file(const char *name, unsigned long *size)
|
|||||||
static void usage(void)
|
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("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,
|
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 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 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 *file=0;
|
||||||
const char *out_path=0;
|
const char *out_path=0;
|
||||||
const char *std_root=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
|
/* 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
|
that is accepted and does nothing is worse than one that is not
|
||||||
accepted at all. */
|
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(strcmp(argv[i],"--strip-error-names")==0) { }
|
||||||
else if(argv[i][0]!='-') file=argv[i];
|
else if(argv[i][0]!='-') file=argv[i];
|
||||||
else if(strcmp(argv[i],"--help")==0){usage();return 0;}
|
else if(strcmp(argv[i],"--help")==0){usage();return 0;}
|
||||||
@@ -103,6 +108,11 @@ int main(int argc, char **argv)
|
|||||||
if(ok){
|
if(ok){
|
||||||
fe_check_init(&check,&build,&d,pointer_bits,no_checks);
|
fe_check_init(&check,&build,&d,pointer_bits,no_checks);
|
||||||
if(!fe_check_program(&check)) ok=0;
|
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)){
|
if(ok && (dump_ir||emit_asm)){
|
||||||
FeIrModule ir;
|
FeIrModule ir;
|
||||||
fe_ir_module_init(&ir);
|
fe_ir_module_init(&ir);
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#include "report.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef FE_REPORT_H
|
||||||
|
#define FE_REPORT_H
|
||||||
|
|
||||||
|
#include "check.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* 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
|
||||||
+34
-1
@@ -34,7 +34,7 @@ FIXTURES = ROOT / "fec" / "tests"
|
|||||||
WATCOM = ROOT / ".dosboxx" / "watcom"
|
WATCOM = ROOT / ".dosboxx" / "watcom"
|
||||||
SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own",
|
SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own",
|
||||||
"check", "checkexp", "checkstm", "checkgen", "checkcal", "checkpro",
|
"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+):)?(.*)$")
|
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)
|
marked = sum(1 for p in cases if expectation(p).line is not None)
|
||||||
print(f"\n{len(cases) - len(failed)}/{len(cases)} passed "
|
print(f"\n{len(cases) - len(failed)}/{len(cases)} passed "
|
||||||
f"({marked} pin a line and message)")
|
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
|
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__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user