From 32ad50b14a78a28320a1abbdca76f3c0da5a3bb6 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 02:22:44 +0900 Subject: [PATCH] dev: run the host gate as 16-bit, and drop what it found dead The gate was using a system-wide Open Watcom that only ships the 32-bit compiler. The project downloads its own toolchain, and .dosboxx/watcom/binnt has the Windows-hosted 16-bit wcl.exe -- the same compiler and the same target as the DOS build. Run that instead, with build-dos.bat's exact command. The difference is not academic. Compiling 16-bit immediately reported three functions the unification had orphaned: emit_type_helpers, which the M7 program emitter replaced, and emit_drop_helpers and emit_drop_fields, which it was the only caller of. The 32-bit check had been clean. Those warnings were going to the DOS screen, where the runner cannot see them: COMMAND.COM redirects handle 1 only, and Watcom writes diagnostics to handle 2. So the suite was green while the build was not quiet. Nothing in the runner would ever have said so. Found while chasing W210/W107 reported from a DOS screen, which this does not yet explain -- those are not among what the compiler build emits now. M1-M7: 214 passed. --- fec/src/emit_c.c | 157 -------------------------------- tools/tests/test_host_syntax.py | 38 +++++--- 2 files changed, 24 insertions(+), 171 deletions(-) diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index b168c39..1fc185b 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -111,74 +111,7 @@ static FeNode *find_drop_method(FeEmitter *e, const char *name) return 0; } -static void emit_drop_fields(FeEmitter *e, FeType *t) -{ - unsigned i; - FeType *ft; - for (i=t->field_count; i>0; --i) { - ft=t->fields[i-1].type; - if (!type_needs_drop(ft)) continue; - if (ft->kind==FE_TYPE_OWNED) { - fputs("if (self->",e->out); fputs(t->fields[i-1].name,e->out); - if(ft->elem && ft->elem->kind==FE_TYPE_SLICE) fputs(".p",e->out); - fputs(") { ",e->out); - if(ft->elem && ft->elem->kind==FE_TYPE_SLICE) { - fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs(".p); self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs(".p=0; ",e->out); - } else if (ft->elem && type_needs_drop(ft->elem) && ft->elem->drop_cname) { - fprintf(e->out,"%s(self->%s); ",ft->elem->drop_cname,t->fields[i-1].name); - } - if(!(ft->elem && ft->elem->kind==FE_TYPE_SLICE)) { - fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs("); self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs("=0; ",e->out); - } - fputs("}\n",e->out); - } else if (ft->kind==FE_TYPE_STRUCT && ft->drop_cname) { - fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); - } else if (ft->kind==FE_TYPE_ARRAY && ft->drop_cname) { - fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); - } - } -} -static void emit_drop_helpers(FeEmitter *e) -{ - FeType *t; - FeNode *method; - for (t=e->check->types.types; t; t=t->next) - if(t->kind==FE_TYPE_STRUCT && (method=find_drop_method(e,t->name))!=0) - fprintf(e->out,"void %s(%s *self);\n", - cname(method,"fe_drop_method"),t->cname); - for (t=e->check->types.types; t; t=t->next) - if ((t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY) && - type_needs_drop(t) && t->drop_cname) - fprintf(e->out,"static void %s(%s *self);\n",t->drop_cname,t->cname); - for (t=e->check->types.types; t; t=t->next) { - if (t->kind!=FE_TYPE_STRUCT || !type_needs_drop(t) || !t->drop_cname) continue; - fprintf(e->out,"static void %s(%s *self) {\n",t->drop_cname,t->cname); - method=find_drop_method(e,t->name); - if (method) { - fprintf(e->out,"%s(self);\n",cname(method,"fe_drop_method")); - } - emit_drop_fields(e,t); - fputs("}\n",e->out); - } - for (t=e->check->types.types; t; t=t->next) - if (t->kind==FE_TYPE_ARRAY && type_needs_drop(t) && t->drop_cname) { - fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", - t->drop_cname,t->cname,t->length); - if (t->elem->kind==FE_TYPE_OWNED) { - fputs("if (self->a[i]) { ",e->out); - if (t->elem->elem && type_needs_drop(t->elem->elem) && t->elem->elem->drop_cname) - fprintf(e->out,"%s(self->a[i]); ",t->elem->elem->drop_cname); - fputs("free(self->a[i]); self->a[i]=0; }",e->out); - } else if (t->elem->drop_cname) - fprintf(e->out,"%s(&self->a[i]);",t->elem->drop_cname); - fputs(" } }\n",e->out); - } -} /* 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 @@ -196,96 +129,6 @@ static int node_uses_trim(FeNode *n) return 0; } -static void emit_type_helpers(FeEmitter *e) -{ - FeType *t; - unsigned i,j; - for(t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && - t->error_value->kind!=FE_TYPE_VOID) { - fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", - t->cname,t->maker,fe_type_c_name(t->error_value,e->pointer_bits),t->cname); - if (t->error_value->kind==FE_TYPE_OWNED && - t->error_value->elem && - t->error_value->elem->kind==FE_TYPE_SLICE) { - FeType *item=t->error_value->elem->elem; - fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", - t->cname,t->alloc_cname,t->cname, - fe_type_c_name(item,e->pointer_bits), - fe_type_c_name(item,e->pointer_bits)); - } else if (t->error_value->kind==FE_TYPE_OWNED) { - fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", - t->cname,t->alloc_cname, - fe_type_c_name(t->error_value->elem,e->pointer_bits),t->cname, - fe_type_c_name(t->error_value,e->pointer_bits), - fe_type_c_name(t->error_value->elem,e->pointer_bits)); - } - } - } - for(t=e->check->types.types;t;t=t->next) { - if(t->replace_cname) { - const char *ct=fe_type_c_name(t,e->pointer_bits); - fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", - ct,t->replace_cname,ct,ct,ct); - } - if(t->kind==FE_TYPE_STRUCT && t->maker) { - fprintf(e->out,"static %s %s(",t->cname,t->maker); - for(i=0;ifield_count;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->fields[i].type,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } - fputs(") { ",e->out); fprintf(e->out,"%s v;",t->cname); - for(i=0;ifield_count;i++) fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); - fputs(" return v; }\n",e->out); - } else if(t->kind==FE_TYPE_ARRAY && t->maker) { - fprintf(e->out,"static %s %s(",t->cname,t->maker); - for(i=0;ilength;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } - fputs(") { ",e->out); fprintf(e->out,"%s v;",t->cname); - for(i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); - fputs(" return v; }\n",e->out); - } else if(t->kind==FE_TYPE_ENUM) { - for(i=0;ivariant_count;i++) { - FeVariantType *v=&t->variants[i]; - fprintf(e->out,"static %s %s(",t->cname,v->maker); - for(j=0;jfield_count;j++) { if(j) fputs(", ",e->out); fputs(fe_type_c_name(v->fields[j].type,e->pointer_bits),e->out); fprintf(e->out," p%u",j); } - fputs(") { ",e->out); fprintf(e->out,"%s x; x.tag=%u;",t->cname,v->tag); - for(j=0;jfield_count;j++) { if(v->field_count==1) fprintf(e->out," x.payload.%s=p%u;",v->name,j); else fprintf(e->out," x.payload.%s.%s=p%u;",v->name,v->fields[j].name,j); } - fputs(" return x; }\n",e->out); - } - } - } - emit_drop_helpers(e); - for(t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_ARRAY && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); - if(!e->no_checks) fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); - fprintf(e->out,"return x.a[i]; }\n"); - fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", - fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->slicer,t->cname); - if(!e->no_checks) fputs("if (a > b || b > ",e->out), fprintf(e->out,"%lu",t->length), fputs(") fe_trap_bounds(); ",e->out); - fprintf(e->out,"return %s(x->a+a,b-a); }\n",fe_type_slice(&e->check->types,t->elem)->maker); - fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->full_slicer,t->cname,t->slicer,t->length); - fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->tail_slicer,t->cname,t->slicer,t->length); - } else if (t->kind==FE_TYPE_SLICE && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); - if(!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); - fputs("return x.p[i]; }\n",e->out); - fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", - fe_type_c_name(t,e->pointer_bits),t->slicer,t->cname); - if(!e->no_checks) fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); - 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 (aa && (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); - } - } - } -} static void emit_m4_runtime(FeEmitter *e) { diff --git a/tools/tests/test_host_syntax.py b/tools/tests/test_host_syntax.py index 56408e5..8fb4972 100644 --- a/tools/tests/test_host_syntax.py +++ b/tools/tests/test_host_syntax.py @@ -7,13 +7,17 @@ declaration or a signature that disagrees with its definition used to surface only after a DOSBox-X boot and a full compiler build; here it surfaces in about a second, with the line number. -It uses the same Open Watcom the project targets, just the Windows-hosted build, -and the same strictness as ``fec/build-dos.bat`` (``-za -wx -wcd=202``). The -target differs -- wcc386 is 32-bit where the DOS build is 16-bit large model -- -so this catches syntax, types and declarations, not code generation or memory -model problems. +It runs the pinned toolchain's Windows-hosted 16-bit driver with the exact +command ``fec/build-dos.bat`` uses, so the diagnostics match what the DOS build +sees. A 32-bit compile is not equivalent: it misses warnings that only the +16-bit large model reports, which is how a dead function survived the M7 +unification with a clean 32-bit check. -Skipped when Watcom is not installed, so the suite still runs anywhere. +What it still cannot see is the DOS environment itself -- memory limits, the +command line length, the filesystem. The DOS build and the milestone suite +remain the gate. + +Skipped when the toolchain has not been downloaded, so the suite runs anywhere. """ from __future__ import annotations @@ -34,11 +38,13 @@ SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", def _watcom() -> Path | None: + """Prefer the toolchain the runner pins over anything installed system-wide.""" root = os.environ.get("WATCOM") - candidates = [Path(root)] if root else [] - candidates.append(Path("C:/WATCOM19")) + candidates = [ROOT / ".dosboxx" / "watcom"] + if root: + candidates.append(Path(root)) for base in candidates: - if (base / "binnt" / "wcc386.exe").is_file(): + if (base / "binnt" / "wcl.exe").is_file(): return base return None @@ -47,7 +53,7 @@ def _watcom() -> Path | None: def watcom() -> Path: base = _watcom() if base is None: - pytest.skip("Open Watcom is not installed on the host; set WATCOM to enable") + pytest.skip("run `ferro-dos setup` to download the pinned toolchain") return base @@ -64,14 +70,18 @@ def test_source_compiles_clean(name: str, watcom: Path, objdir: Path) -> None: env = dict(os.environ) env["WATCOM"] = os.fspath(watcom) env["INCLUDE"] = os.fspath(watcom / "h") + env["PATH"] = os.pathsep.join( + [os.fspath(watcom / "binnt"), env.get("PATH", "")]) + # The same command build-dos.bat runs, minus the object name. completed = subprocess.run( - [os.fspath(watcom / "binnt" / "wcc386.exe"), "-q", "-za", "-wx", - "-wcd=202", "-zq", f"-i={SRC}", os.fspath(source)], + [os.fspath(watcom / "binnt" / "wcl.exe"), "-q", "-za", "-wx", + "-bt=dos", "-ml", "-k32768", "-c", f"-i={SRC}", os.fspath(source)], cwd=objdir, capture_output=True, text=True, env=env, timeout=120, ) output = (completed.stdout + completed.stderr).strip() - # -wx keeps warnings meaningful, so treat any diagnostic as a failure: the - # DOS build runs the same flags and stops on them. + # -wx keeps warnings meaningful, so treat any diagnostic as a failure. The + # DOS build prints them to a screen nobody reads, which is how they + # accumulate unnoticed. assert completed.returncode == 0 and not output, ( f"{name}.c does not compile clean\n{output}" )