diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 0a053dc..0e919a1 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -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 (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); + } } } } @@ -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 && diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index c968663..1a30e0f 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -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(): diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index e967920..5a47497 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -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)