diff --git a/fec/rt/start.asm b/fec/rt/start.asm new file mode 100644 index 0000000..9d6860b --- /dev/null +++ b/fec/rt/start.asm @@ -0,0 +1,134 @@ +; Ferro runtime: process entry and the trap handler. +; +; The entry point calls the program's `main` and hands its result to +; ExitProcess, so a Ferro program is an ordinary console executable. +; `fe_trap` prints where the program stopped and why, then exits 3. + +.386 +.model flat + +extern _ExitProcess@4 : near +extern _GetStdHandle@4 : near +extern _WriteFile@20 : near +extern fe_main_ : near + +_DATA segment dword public 'DATA' + +reasons dd offset r_bounds, offset r_overflow, offset r_divide + dd offset r_unreach, offset r_explicit +r_bounds db 'index out of bounds',0 +r_overflow db 'integer overflow',0 +r_divide db 'divide by zero',0 +r_unreach db 'reached unreachable code',0 +r_explicit db 'trap',0 +r_unknown db 'trap',0 +prefix db 'ferro: ',0 +at_word db ' at ',0 +colon db ':',0 +newline db 13,10,0 +numbuf db 16 dup(0) +written dd 0 + +_DATA ends + +_TEXT segment dword public 'CODE' + +; write_cstr(esi = pointer to a NUL-terminated string) -> void +write_cstr proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, esi + xor ecx, ecx +count_loop: + cmp byte ptr [edi], 0 + je count_done + inc edi + inc ecx + jmp count_loop +count_done: + test ecx, ecx + je write_done + push -11 ; STD_ERROR_HANDLE + call _GetStdHandle@4 + push 0 ; lpOverlapped + push offset written + push ecx + push esi + push eax + call _WriteFile@20 +write_done: + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +write_cstr endp + +; write_uint(eax = value) -> void +write_uint proc near + push ebp + mov ebp, esp + push ebx + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov ebx, 10 +digit_loop: + xor edx, edx + div ebx + add dl, '0' + dec edi + mov [edi], dl + test eax, eax + jnz digit_loop + mov esi, edi + call write_cstr + pop ebx + mov esp, ebp + pop ebp + ret +write_uint endp + +; fe_trap(reason, file, line) -- cdecl, never returns +public fe_trap +fe_trap proc near + push ebp + mov ebp, esp + mov esi, offset prefix + call write_cstr + mov eax, [ebp+8] ; reason + cmp eax, 5 + jb reason_ok + mov esi, offset r_unknown + jmp reason_write +reason_ok: + mov esi, [reasons + eax*4] +reason_write: + call write_cstr + mov esi, offset at_word + call write_cstr + mov esi, [ebp+12] ; file + call write_cstr + mov esi, offset colon + call write_cstr + mov eax, [ebp+16] ; line + call write_uint + mov esi, offset newline + call write_cstr + push 3 + call _ExitProcess@4 +fe_trap endp + +public fe_start_ +fe_start_ proc near + call fe_main_ + push eax + call _ExitProcess@4 +fe_start_ endp + +_TEXT ends + +end fe_start_ diff --git a/fec/src/driver.c b/fec/src/driver.c index cd18a5e..7a15a81 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -2,6 +2,7 @@ #include "check.h" #include "resolve.h" #include "lower.h" +#include "x86.h" #include #include #include @@ -17,7 +18,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] file.fe [--no-checks]"); + puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--no-checks]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -38,8 +39,9 @@ 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; + int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; const char *file=0; + const char *out_path=0; unsigned long n; char *src; FeDiags d; @@ -53,13 +55,15 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"--dump-tokens")==0) dump_tok=1; else if(strcmp(argv[i],"--check")==0) check_only=1; else if(strcmp(argv[i],"--dump-ir")==0) dump_ir=1; + else if(strcmp(argv[i],"--emit-asm")==0) emit_asm=1; + else if(strcmp(argv[i],"-o")==0 && i+11){ + if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(dump_ir?1:0)+(emit_asm?1:0)>1){ fprintf(fe_diag_stream(),"fec: choose only one output mode\n"); return 2; } @@ -94,11 +98,16 @@ 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; - if(ok && dump_ir){ + if(ok && (dump_ir||emit_asm)){ FeIrModule ir; fe_ir_module_init(&ir); if(!fe_lower_program(&check,&ir)) ok=0; - else fe_ir_dump(&ir,stdout); + else if(dump_ir) fe_ir_dump(&ir,stdout); + else { + FILE *o=out_path?fopen(out_path,"w"):stdout; + if(!o){fprintf(fe_diag_stream(),"fec: cannot write %s\n",out_path);ok=0;} + else { fe_x86_emit(&ir,o); if(out_path) fclose(o); } + } fe_ir_module_destroy(&ir); } fe_check_destroy(&check); diff --git a/fec/src/ir.c b/fec/src/ir.c index aa21351..b5ebb20 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -5,6 +5,7 @@ void fe_ir_module_init(FeIrModule *m) { fe_arena_init(&m->arena, 16384); m->unit_file = ""; + m->entry_main = 0; m->funcs = 0; m->last_func = 0; m->globals = 0; diff --git a/fec/src/ir.h b/fec/src/ir.h index 2829f95..38b5acb 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -133,6 +133,9 @@ typedef struct FeIrGlobal { typedef struct FeIrModule { FeArena arena; const char *unit_file; /* the one file-name string a unit's traps share */ + /* The entry unit's `main`, if it has one. The runtime's start stub + calls a fixed name, so the generator emits a jump to this one. */ + const char *entry_main; FeIrFunc *funcs; FeIrFunc *last_func; FeIrGlobal *globals; diff --git a/fec/src/lower.c b/fec/src/lower.c index ac9b95b..23926ea 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -582,7 +582,12 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) c->types.unit_name = unit->name[0] ? unit->name : "unit"; if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) - if (n->kind == FE_N_FN && n->c) lower_fn(&L, n); + if (n->kind == FE_N_FN && n->c) { + lower_fn(&L, n); + /* The entry unit is the one the build was rooted at. */ + if (u == 0 && n->text && !strcmp(n->text, "main")) + out->entry_main = n->cname; + } } return !L.failed; } diff --git a/fec/src/x86.c b/fec/src/x86.c new file mode 100644 index 0000000..11c6ef0 --- /dev/null +++ b/fec/src/x86.c @@ -0,0 +1,384 @@ +#include "x86.h" +#include + +/* ------------------------------------------------------------------------- * + * i386 code generation + * + * The frame, from EBP downwards: + * + * [ebp + 8 + 4k] incoming argument k + * [ebp + 4] return address + * [ebp] saved ebp + * [ebp - ...] parameters, copied in from the argument area + * [ebp - ...] locals + * [ebp - ...] one slot per temporary + * + * Parameters are copied into the frame rather than read in place so that a + * parameter and a local are the same thing to everything below. + * ------------------------------------------------------------------------- */ + +typedef struct Frame { + const FeIrFunc *f; + long *local_off; /* [ebp + off] for each local */ + long temp_base; /* first temporary slot */ + long size; /* bytes to subtract from esp */ +} Frame; + +static long align_up(long v, long a) +{ + long r = v % a; + return r ? v + a - r : v; +} + +static unsigned long slot_bytes(const FeIrLocal *l) +{ + switch (l->type) { + case FE_IR_I8: return 1; + case FE_IR_I16: return 2; + case FE_IR_I32: return 4; + case FE_IR_PTR: return 4; + case FE_IR_MEM: return l->size ? l->size : 1; + default: return 4; + } +} + +/* Every temporary is four bytes: a temporary only ever holds something that + fits in a register, and narrower values are kept zero- or sign-extended. */ +#define TEMP_SLOT 4L + +static void frame_layout(Frame *fr, const FeIrFunc *f, long *storage) +{ + unsigned i; + long off = 0; + fr->f = f; + fr->local_off = storage; + for (i = 0; i < f->local_count; ++i) { + unsigned long size = slot_bytes(&f->locals[i]); + long a = (long)f->locals[i].align; + if (a < 1) a = 1; + if (a > 4) a = 4; + off = align_up(off + (long)size, a); + storage[i] = -off; + } + off = align_up(off, 4); + fr->temp_base = -off; + off += (long)f->temp_count * TEMP_SLOT; + fr->size = align_up(off, 4); +} + +static long temp_off(const Frame *fr, unsigned t) +{ + return fr->temp_base - (long)(t + 1) * TEMP_SLOT; +} + +static const char *word_of(FeIrType t) +{ + switch (t) { + case FE_IR_I8: return "byte ptr"; + case FE_IR_I16: return "word ptr"; + default: return "dword ptr"; + } +} + +static const char *reg_of(FeIrType t, int which) +{ + /* which: 0 -> a, 1 -> c, 2 -> d */ + switch (t) { + case FE_IR_I8: return which == 0 ? "al" : which == 1 ? "cl" : "dl"; + case FE_IR_I16: return which == 0 ? "ax" : which == 1 ? "cx" : "dx"; + default: return which == 0 ? "eax" : which == 1 ? "ecx" : "edx"; + } +} + +/* Write the effective address of a place into `buf`. A place is a base plus a + constant, and the only base that is not already an address is a temporary, + which holds a pointer. */ +static void place_addr(const Frame *fr, const FeIrPlace *p, char *buf) +{ + switch (p->base) { + case FE_PLACE_LOCAL: + sprintf(buf, "[ebp%+ld]", fr->local_off[p->index] + p->offset); + break; + case FE_PLACE_GLOBAL: + if (p->offset) sprintf(buf, "[%s%+ld]", p->name, p->offset); + else sprintf(buf, "[%s]", p->name); + break; + case FE_PLACE_TEMP: + sprintf(buf, "[edx%+ld]", p->offset); + break; + } +} + +/* A temporary-based place needs its pointer in a register first. */ +static void load_place_base(const Frame *fr, const FeIrPlace *p, FILE *out) +{ + if (p->base != FE_PLACE_TEMP) return; + fprintf(out, " mov edx, [ebp%+ld]\n", temp_off(fr, p->index)); +} + +static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) +{ + fprintf(out, " mov %s, [ebp%+ld]\n", reg, temp_off(fr, t)); +} + +static void store_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) +{ + fprintf(out, " mov [ebp%+ld], %s\n", temp_off(fr, t), reg); +} + +static const char *cmp_set(FeIrOp op, int is_unsigned) +{ + switch (op) { + case FE_IR_EQ: return "sete"; + case FE_IR_NE: return "setne"; + case FE_IR_LT: return is_unsigned ? "setb" : "setl"; + case FE_IR_LE: return is_unsigned ? "setbe" : "setle"; + case FE_IR_GT: return is_unsigned ? "seta" : "setg"; + case FE_IR_GE: return is_unsigned ? "setae" : "setge"; + default: return "sete"; + } +} + +static void emit_binary(const Frame *fr, const FeIrValue *v, FILE *out) +{ + int is_cmp = v->op >= FE_IR_EQ && v->op <= FE_IR_GE; + FeIrType t = is_cmp ? (FeIrType)v->imm : v->type; + const char *a = reg_of(t, 0); + const char *c = reg_of(t, 1); + load_temp(fr, v->a, "eax", out); + load_temp(fr, v->b, "ecx", out); + if (is_cmp) { + fprintf(out, " cmp %s, %s\n", a, c); + fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); + fprintf(out, " movzx eax, al\n"); + store_temp(fr, v->dest, "eax", out); + return; + } + switch (v->op) { + case FE_IR_ADD: fprintf(out, " add %s, %s\n", a, c); break; + case FE_IR_SUB: fprintf(out, " sub %s, %s\n", a, c); break; + case FE_IR_MUL: fprintf(out, " imul %s, %s\n", a, c); break; + case FE_IR_AND: fprintf(out, " and %s, %s\n", a, c); break; + case FE_IR_OR: fprintf(out, " or %s, %s\n", a, c); break; + case FE_IR_XOR: fprintf(out, " xor %s, %s\n", a, c); break; + case FE_IR_SHL: fprintf(out, " shl %s, cl\n", a); break; + case FE_IR_SHR: + fprintf(out, " %s %s, cl\n", + v->is_unsigned ? "shr" : "sar", a); + break; + case FE_IR_DIV: + case FE_IR_MOD: + /* The divide instructions use edx:eax, so the operands have to be + widened to 32 bits whatever the declared width is. */ + if (v->is_unsigned) fprintf(out, " xor edx, edx\n"); + else fprintf(out, " cdq\n"); + fprintf(out, " %s ecx\n", v->is_unsigned ? "div " : "idiv"); + if (v->op == FE_IR_MOD) fprintf(out, " mov eax, edx\n"); + break; + default: break; + } + store_temp(fr, v->dest, "eax", out); +} + +static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) +{ + char addr[128]; + unsigned i; + switch (v->op) { + case FE_IR_CONST: + fprintf(out, " mov eax, %ld\n", v->imm); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_LOAD: + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + if (v->type == FE_IR_I8) + fprintf(out, " movzx eax, byte ptr %s\n", addr); + else if (v->type == FE_IR_I16) + fprintf(out, " movzx eax, word ptr %s\n", addr); + else + fprintf(out, " mov eax, dword ptr %s\n", addr); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_STORE: + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + load_temp(fr, v->a, "eax", out); + fprintf(out, " mov %s %s, %s\n", word_of(v->type == FE_IR_VOID + ? FE_IR_I32 : v->type), addr, reg_of(v->type == FE_IR_VOID + ? FE_IR_I32 : v->type, 0)); + break; + case FE_IR_ADDR: + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + fprintf(out, " lea eax, %s\n", addr); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_CAST: + load_temp(fr, v->a, "eax", out); + /* Narrowing is free once everything is kept in a 32-bit slot; widening + has to say whether the top bits are copies of the sign. */ + if (v->type == FE_IR_I8) + fprintf(out, " %s eax, al\n", + v->is_unsigned ? "movzx" : "movsx"); + else if (v->type == FE_IR_I16) + fprintf(out, " %s eax, ax\n", + v->is_unsigned ? "movzx" : "movsx"); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_CALL: + /* cdecl: arguments pushed right to left, the caller pops them. */ + for (i = v->arg_count; i > 0; --i) { + load_temp(fr, v->args[i - 1], "eax", out); + fprintf(out, " push eax\n"); + } + fprintf(out, " call %s\n", v->callee); + if (v->arg_count) + fprintf(out, " add esp, %u\n", v->arg_count * 4U); + if (v->has_dest) store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_COPY: { + char dst[128]; + char src[128]; + /* The source base and the destination base both want edx, so a + temporary-based place is resolved into esi or edi first. */ + if (v->place2.base == FE_PLACE_TEMP) { + load_temp(fr, v->place2.index, "esi", out); + sprintf(src, "[esi%+ld]", v->place2.offset); + } else { + place_addr(fr, &v->place2, src); + } + if (v->place.base == FE_PLACE_TEMP) { + load_temp(fr, v->place.index, "edi", out); + sprintf(dst, "[edi%+ld]", v->place.offset); + } else { + place_addr(fr, &v->place, dst); + } + fprintf(out, " lea esi, %s\n", src); + fprintf(out, " lea edi, %s\n", dst); + fprintf(out, " mov ecx, %ld\n", v->imm); + fprintf(out, " cld\n"); + fprintf(out, " rep movsb\n"); + break; + } + default: + emit_binary(fr, v, out); + break; + } +} + +static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) +{ + Frame fr; + long storage[512]; + const FeIrBlock *b; + const FeIrValue *v; + unsigned i; + long arg = 8; + if (f->is_extern || !f->first) return; + if (f->local_count > 512) return; + frame_layout(&fr, f, storage); + + fprintf(out, "\npublic %s\n", f->name); + fprintf(out, "%s proc near\n", f->name); + fprintf(out, " push ebp\n"); + fprintf(out, " mov ebp, esp\n"); + if (fr.size) fprintf(out, " sub esp, %ld\n", fr.size); + fprintf(out, " push esi\n push edi\n"); + /* Copy the incoming arguments into the frame. */ + for (i = 0; i < f->param_count; ++i) { + fprintf(out, " mov eax, [ebp+%ld]\n", arg); + fprintf(out, " mov %s [ebp%+ld], %s\n", + word_of(f->locals[i].type), storage[i], + reg_of(f->locals[i].type, 0)); + arg += 4; + } + + for (b = f->first; b; b = b->next) { + fprintf(out, "L%s_%u:\n", f->name, b->id); + for (v = b->first; v; v = v->next) emit_value(&fr, v, out); + switch (b->term) { + case FE_IR_JMP: + fprintf(out, " jmp L%s_%u\n", f->name, b->target); + break; + case FE_IR_BR: + load_temp(&fr, b->cond, "eax", out); + fprintf(out, " test eax, eax\n"); + fprintf(out, " jnz L%s_%u\n", f->name, b->target); + fprintf(out, " jmp L%s_%u\n", f->name, b->target_else); + break; + case FE_IR_RET: + if (b->has_ret_value) load_temp(&fr, b->ret_value, "eax", out); + fprintf(out, " pop edi\n pop esi\n"); + fprintf(out, " mov esp, ebp\n pop ebp\n"); + fprintf(out, " ret\n"); + break; + case FE_IR_TRAP: + fprintf(out, " push %lu\n", b->trap_line); + fprintf(out, " push offset FE_UNIT_FILE\n"); + fprintf(out, " push %u\n", (unsigned)b->trap); + fprintf(out, " call fe_trap\n"); + fprintf(out, " add esp, 12\n"); + break; + } + } + fprintf(out, "%s endp\n", f->name); + (void)m; +} + +static void emit_string(const char *s, FILE *out) +{ + int in = 0; + fputs(" db ", out); + for (; s && *s; ++s) { + unsigned char c = (unsigned char)*s; + if (c >= 32 && c < 127 && c != '\'' && c != '"') { + if (!in) { fputc('\'', out); in = 1; } + fputc(c, out); + } else { + if (in) { fputs("',", out); in = 0; } + fprintf(out, "%u,", c); + } + } + if (in) fputc('\'', out); + else fputc('0', out); + if (in) fputs(",0", out); + fputc('\n', out); +} + +void fe_x86_emit(const FeIrModule *m, FILE *out) +{ + const FeIrFunc *f; + const FeIrGlobal *g; + int any_trap = 0; + const FeIrBlock *b; + + for (f = m->funcs; f && !any_trap; f = f->next) + for (b = f->first; b; b = b->next) + if (b->term == FE_IR_TRAP) { any_trap = 1; break; } + + fputs(".386\n.model flat\n\n", out); + for (f = m->funcs; f; f = f->next) + if (f->is_extern || !f->first) + fprintf(out, "extern %s : near\n", f->name); + if (any_trap) fputs("extern fe_trap : near\n", out); + + fputs("\n_DATA segment dword public 'DATA'\n", out); + if (any_trap) { + fputs("public FE_UNIT_FILE\nFE_UNIT_FILE label byte\n", out); + emit_string(m->unit_file, out); + } + for (g = m->globals; g; g = g->next) { + fprintf(out, "public %s\n%s label byte\n", g->name, g->name); + fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); + } + fputs("_DATA ends\n", out); + + fputs("\n_TEXT segment dword public 'CODE'\n", out); + for (f = m->funcs; f; f = f->next) emit_func(m, f, out); + /* The runtime's entry stub calls one fixed name, so point it here. */ + if (m->entry_main) + fprintf(out, "\npublic fe_main_\nfe_main_ proc near\n" + " jmp %s\nfe_main_ endp\n", m->entry_main); + fputs("\n_TEXT ends\n\nend\n", out); +} diff --git a/fec/src/x86.h b/fec/src/x86.h new file mode 100644 index 0000000..9f3229e --- /dev/null +++ b/fec/src/x86.h @@ -0,0 +1,16 @@ +#ifndef FE_X86_H +#define FE_X86_H + +#include "ir.h" + +/* IR to i386 assembly, in the syntax Open Watcom's `wasm` accepts. + + There is no register allocator. Every temporary gets a stack slot, and every + instruction loads its operands into fixed registers, computes, and stores + the result back. That is slow code and obviously correct code, and correct + comes first: a register allocator can be dropped in later without the rest + of the compiler noticing, because it only changes where a temporary lives. */ + +void fe_x86_emit(const FeIrModule *m, FILE *out); + +#endif diff --git a/tests/build.py b/tests/build.py new file mode 100644 index 0000000..363b349 --- /dev/null +++ b/tests/build.py @@ -0,0 +1,103 @@ +"""Compile a Ferro program to a Windows executable and run it. + + fec --emit-asm -> wasm -> wlink (+ the runtime, + kernel32) -> .exe + +The toolchain is the pinned Open Watcom under `.dosboxx/watcom`, hosted: the +assembler and linker there produce PE binaries as happily as they produce DOS +ones. Nothing about this step needs a virtual machine. +""" +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +WATCOM = ROOT / ".dosboxx" / "watcom" +RUNTIME = ROOT / "fec" / "rt" / "start.asm" + + +def _env() -> dict: + env = dict(os.environ) + env.update(WATCOM=str(WATCOM), + INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}", + PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}") + return env + + +def _run(cmd, cwd) -> subprocess.CompletedProcess: + return subprocess.run([str(c) for c in cmd], cwd=cwd, env=_env(), + capture_output=True, text=True, timeout=120) + + +def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False): + """Returns (exe_path, log). exe_path is None when a step failed.""" + out_dir.mkdir(parents=True, exist_ok=True) + stem = source.stem + asm = out_dir / f"{stem}.asm" + log = [] + + cmd = [fec, "--emit-asm", source, "-o", asm] + if no_checks: + cmd.append("--no-checks") + step = _run(cmd, out_dir) + log.append(("fec", step.returncode, step.stdout + step.stderr)) + if step.returncode != 0 or not asm.is_file(): + return None, log + + wasm = WATCOM / "binnt" / "wasm.exe" + for src, obj in ((asm, out_dir / f"{stem}.obj"), + (RUNTIME, out_dir / "start.obj")): + step = _run([wasm, "-q", "-zq", src, f"-fo={obj}"], out_dir) + log.append(("wasm " + src.name, step.returncode, + step.stdout + step.stderr)) + if step.returncode != 0: + return None, log + + exe = out_dir / f"{stem}.exe" + step = _run([WATCOM / "binnt" / "wlink.exe", + "system", "nt", + "file", out_dir / f"{stem}.obj", + "file", out_dir / "start.obj", + "library", WATCOM / "lib386" / "nt" / "kernel32.lib", + "name", exe, + "option", "quiet"], out_dir) + log.append(("wlink", step.returncode, step.stdout + step.stderr)) + if step.returncode != 0 or not exe.is_file(): + return None, log + return exe, log + + +def run(exe: Path): + done = subprocess.run([str(exe)], capture_output=True, text=True, + timeout=30) + return done.returncode, done.stdout + done.stderr + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: build.py [--no-checks]") + return 2 + source = Path(sys.argv[1]).resolve() + fec = ROOT / ".build" / "fec.exe" + if not fec.is_file(): + print("build the front end first: uv run python tests/run.py") + return 2 + exe, log = build(fec, source, ROOT / ".build" / "out", + "--no-checks" in sys.argv) + for name, code, text in log: + if code != 0 or text.strip(): + print(f"--- {name} (exit {code})") + print(text.rstrip()) + if not exe: + return 1 + code, text = run(exe) + if text: + print(text, end="") + print(f"{exe.name} exited {code}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/run.py b/tests/run.py index 7f841ba..84cd013 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ ROOT = Path(__file__).resolve().parent.parent FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "resolve", "ir", "lower", "driver") + "check", "resolve", "ir", "lower", "x86", "driver") # Fixtures live here until there is a code generator to run them against. QUARANTINE = "pending-backend"