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:
2026-08-17 16:31:57 +09:00
parent b0cc737c6b
commit f06a12f341
4 changed files with 177 additions and 1 deletions
+10
View File
@@ -3,6 +3,7 @@
#include "resolve.h"
#include "lower.h"
#include "x86.h"
#include "report.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -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);
+120
View File
@@ -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);
}
+13
View File
@@ -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
View File
@@ -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())