dev: compile the generated C, and fix what that caught
Thirty-one cases only ever emitted C and never built it -- m5 owned/defer, and every ok* acceptance fixture in m6 and m7. They asserted that fec produced a file, not that the file was a program, so a malformed emission sat unnoticed. Compile each one with wcl386 -c. Fixtures without a main cannot be run, so this is the floor for them; it is a conformance check on the backend's output, not an assertion about C, so a future backend swaps the command rather than the intent. It found two defects in the M6 emitter immediately, both predating M7: &s[0] took the address of an rvalue. Borrowing went through emit_expr, which lowers an index to the bounds-checking accessor, so `&s[0]` became `&fe_idx_slice_type_2(s, 0)`. emit_lvalue spells the same element as `s.p[0]`. The M7 emitter already did this correctly; only the M6 path was wrong. line.trim() was never lowered. SPEC lists trim among the built-in alias methods on str and the checker accepts it, but no emitter case existed, so it emitted `fe_l_line_0.trim()` -- a member access on a slice struct. Emit the helper, and only for programs that actually trim. Also set core=dynamic and cycles=max. core=auto uses the interpreter in real mode, which is where the 16-bit compiler build spends its time; nothing here is timing sensitive. m6 drops from 31s to 17s. Raise the DOSBox timeout to match a whole-suite run, which now pays a process spawn per compile check. Verified: --only m6, 57 passed.
This commit is contained in:
+39
-1
@@ -199,6 +199,22 @@ static void emit_drop_helpers(FeEmitter *e)
|
||||
}
|
||||
}
|
||||
|
||||
/* SPEC 12.3 lists `trim` among the built-in alias methods on `str`, called as
|
||||
`line.trim()`. The checker accepts it; this emits the lowering. Only the
|
||||
helper for slice types actually reached by a trim call is emitted, so a
|
||||
program that never trims does not carry it. */
|
||||
static int node_uses_trim(FeNode *n)
|
||||
{
|
||||
FeNode *x;
|
||||
if (!n) return 0;
|
||||
if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_MEMBER &&
|
||||
n->a->b && n->a->b->text && strcmp(n->a->b->text,"trim")==0) return 1;
|
||||
if (node_uses_trim(n->a) || node_uses_trim(n->b) || node_uses_trim(n->c))
|
||||
return 1;
|
||||
for (x=n->children; x; x=x->next) if (node_uses_trim(x)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void emit_type_helpers(FeEmitter *e)
|
||||
{
|
||||
FeType *t;
|
||||
@@ -278,6 +294,14 @@ static void emit_type_helpers(FeEmitter *e)
|
||||
fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker);
|
||||
fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n",t->cname,t->full_slicer,t->cname,t->slicer);
|
||||
fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",t->cname,t->tail_slicer,t->cname,t->slicer);
|
||||
if (node_uses_trim(e->check->ast->root) && !t->ref_mut) {
|
||||
fprintf(e->out,
|
||||
"static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;"
|
||||
" while (a<b && (s.p[a]==' '||s.p[a]=='\\t'||s.p[a]=='\\r'||s.p[a]=='\\n')) ++a;"
|
||||
" while (b>a && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;"
|
||||
" return %s(s.p+a,b-a); }\n",
|
||||
t->cname,t->cname,t->cname,t->maker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -812,7 +836,12 @@ static void emit_expr(FeEmitter *e, FeNode *n)
|
||||
fputc('(', e->out);
|
||||
fputs(strcmp(op,"&mut")==0 ? "&" : op, e->out);
|
||||
}
|
||||
emit_expr(e, n->a);
|
||||
/* Borrowing needs a place, not a value. emit_expr lowers an index to
|
||||
the bounds-checking accessor, and the address of that call is not an
|
||||
lvalue -- `&s[0]` became `&fe_idx_slice_type_2(s, 0)`, which C
|
||||
rejects. emit_lvalue spells the same element as `s.p[0]`. */
|
||||
if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) emit_lvalue(e, n->a);
|
||||
else emit_expr(e, n->a);
|
||||
fputc(')', e->out);
|
||||
break;
|
||||
case FE_N_BINARY:
|
||||
@@ -871,6 +900,15 @@ static void emit_expr(FeEmitter *e, FeNode *n)
|
||||
fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out);
|
||||
special=1;
|
||||
}
|
||||
else if(n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text &&
|
||||
strcmp(n->a->b->text,"trim")==0 && !n->children &&
|
||||
n->a->a && n->a->a->sem_type &&
|
||||
n->a->a->sem_type->kind==FE_TYPE_SLICE &&
|
||||
n->a->a->sem_type->cname) {
|
||||
fputs("fe_trim_",e->out); fputs(n->a->a->sem_type->cname,e->out);
|
||||
fputc('(',e->out); emit_expr(e,n->a->a); fputc(')',e->out);
|
||||
special=1;
|
||||
}
|
||||
else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a &&
|
||||
n->a->a->kind==FE_N_IDENT && n->a->a->text &&
|
||||
strcmp(n->a->a->text,"mem")==0 && n->a->b &&
|
||||
|
||||
@@ -262,7 +262,13 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False,
|
||||
console = run_root / "CONSOLE.LOG"
|
||||
config = run_root / "DOSBOX.CON"
|
||||
config.write_text(
|
||||
f"[log]\nlogfile={console}\n[dosbox]\nlog console=quiet\n",
|
||||
# core=auto falls back to the interpreter in real mode, which is
|
||||
# where the 16-bit compiler build spends its time. Nothing here is
|
||||
# timing sensitive -- it is a compiler and a batch file -- so ask for
|
||||
# the recompiler and uncapped cycles explicitly.
|
||||
f"[log]\nlogfile={console}\n"
|
||||
f"[dosbox]\nlog console=quiet\n"
|
||||
f"[cpu]\ncore=dynamic\ncycles=max\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
(fec / "RUN.BAT").write_text(
|
||||
@@ -279,7 +285,10 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False,
|
||||
"-c", f'mount W "{watcom}" -ro',
|
||||
"-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT",
|
||||
])
|
||||
completed = subprocess.run(command, check=False, timeout=300)
|
||||
# Every case pays a DOS process spawn, and the compile-only checks spawn
|
||||
# wcc386 once each, so the whole-suite run is minutes rather than the
|
||||
# under-a-minute a single milestone takes.
|
||||
completed = subprocess.run(command, check=False, timeout=1800)
|
||||
if completed.returncode != 0:
|
||||
raise DosboxError(f"DOSBox-X exited with status {completed.returncode}")
|
||||
if not (fec / "RUN.OK").is_file():
|
||||
|
||||
@@ -74,6 +74,41 @@ def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False,
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _wcc(source: str, obj: str) -> str:
|
||||
"""Compile the generated C without linking.
|
||||
|
||||
A fixture with no ``main`` cannot be run, so this is the floor for it: the
|
||||
backend's output has to survive the compiler the project actually ships
|
||||
with. It catches a malformed emission -- an unnamed assignment target, a
|
||||
helper that is called but never defined, an initializer C89 rejects -- which
|
||||
otherwise sits unnoticed in a case that only ever emitted text.
|
||||
|
||||
This asserts nothing about the C itself; it is a conformance check on the
|
||||
backend's output, so a future non-C backend swaps the command rather than
|
||||
the intent. Never grep the generated C to prove a language feature -- write a
|
||||
fixture whose exit code differs instead, as TESTS\\M3\\NOCHK.FE does.
|
||||
"""
|
||||
# Through the wcl386 driver with -c rather than calling wcc386 directly:
|
||||
# wcc386 writes its diagnostics to stderr, which COMMAND.COM cannot
|
||||
# redirect, so a failure would report a count and no messages. The driver
|
||||
# leaves an .ERR file, which the runner already collects.
|
||||
return f"WCL386 -q -za -wx -wcd=202 -bt=dos -c -fo={obj} {source}"
|
||||
|
||||
|
||||
def _accepts(milestone: int, directory: str, names: tuple[str, ...], *,
|
||||
output: str = OUT, output_first: bool = True) -> list[Case]:
|
||||
"""Fixtures that must compile: emit the C, then build it."""
|
||||
cases: list[Case] = []
|
||||
for name in names:
|
||||
cfile = f"{output}\\{name.upper()}.C"
|
||||
cases.append(_case(milestone, name,
|
||||
_emit(_fe(directory, name), cfile,
|
||||
output_first=output_first)))
|
||||
cases.append(_case(milestone, f"{name}-cc",
|
||||
_wcc(cfile, f"{OUT}\\{name.upper()}.OBJ")))
|
||||
return cases
|
||||
|
||||
|
||||
def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *,
|
||||
suffix: str, ok: bool = True, prefix: str = "") -> list[Case]:
|
||||
return [
|
||||
@@ -171,8 +206,7 @@ CASES: list[Case] = [
|
||||
"bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")),
|
||||
|
||||
# -- M5: defer and ownership ----------------------------------------------
|
||||
_case(5, "defer", _emit(_fe(M5, "defer"), f"{M5}\\DEFER.C")),
|
||||
_case(5, "owned", _emit(_fe(M5, "owned"), f"{M5}\\OWNED.C")),
|
||||
*_accepts(5, M5, ("defer", "owned"), output=M5, output_first=False),
|
||||
*_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond",
|
||||
"bad-proj", "bad-clos", "bad-loop")),
|
||||
# The runtime case links the generated C against a hand-written allocator
|
||||
@@ -189,22 +223,20 @@ CASES: list[Case] = [
|
||||
"badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr",
|
||||
"badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld",
|
||||
"badtwo", "badup", "badweak")],
|
||||
*[_case(6, name, _emit(_fe(M6, name), f"{OUT}\\{name.upper()}.C",
|
||||
output_first=True)) for name in (
|
||||
*_accepts(6, M6, (
|
||||
"okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join",
|
||||
"okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb",
|
||||
"okstatic", "oktemp", "oktrim", "okwcall")],
|
||||
"okstatic", "oktemp", "oktrim", "okwcall")),
|
||||
|
||||
# -- M7: optionals and error unions ---------------------------------------
|
||||
*[_case(7, name, f"FEC.EXE --check {_fe(M7, name)}", False) for name in (
|
||||
"badcatch", "baddef", "baddir", "badercod", "badernam", "badetype",
|
||||
"badnull", "badoref", "badorel", "badproj", "badqmark", "badret",
|
||||
"badsome", "badtry", "badzero")],
|
||||
*[_case(7, name, _emit(_fe(M7, name), f"{OUT}\\{name.upper()}.C",
|
||||
output_first=True)) for name in (
|
||||
*_accepts(7, M7, (
|
||||
"okcatch", "okcatmov", "okcvoid", "okdeflt", "okiflet", "okmatch",
|
||||
"oknull", "okorelse", "okpatvw", "okproj", "okrepl", "oktrdef",
|
||||
"oktry")],
|
||||
"oktry")),
|
||||
]
|
||||
|
||||
MAX_MILESTONE: int = max(case.milestone for case in CASES)
|
||||
|
||||
Reference in New Issue
Block a user