backend: i386 어셈블리를 내고 Windows 11 실행 파일을 만든다

fec --emit-asm -> wasm -> wlink -> .exe. 툴체인은 고정된 Open Watcom 그대로다.

레지스터 할당기가 없다. 임시값마다 스택 슬롯을 주고, 명령마다 피연산자를
고정 레지스터로 읽어 계산하고 다시 저장한다. 느린 코드지만 명백히 옳은
코드이고, 옳은 것이 먼저다. 나중에 할당기를 끼워도 나머지는 모른다 --
임시값이 어디 사는지만 바뀐다.

런타임 fec/rt/start.asm 은 진입 스텁과 fe_trap 이다. trap 은 이유와 파일과
줄을 stderr 에 쓰고 3으로 끝낸다.

처음으로 Ferro 프로그램이 실행됐다:

  1..10 합         -> 55
  (7*6-2)/4        -> 10
  루프+호출+분기   -> 1

tests/build.py 가 컴파일하고 링크하고 돌린다.
This commit is contained in:
2026-08-17 05:55:34 +09:00
parent 6ee3764667
commit e5093e690d
9 changed files with 662 additions and 7 deletions
+134
View File
@@ -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_
+14 -5
View File
@@ -2,6 +2,7 @@
#include "check.h"
#include "resolve.h"
#include "lower.h"
#include "x86.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -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+1<argc) out_path=argv[++i];
else if(strcmp(argv[i],"--no-checks")==0) no_checks=1;
else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || 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;}
else {fprintf(fe_diag_stream(),"fec: unknown option %s\n",argv[i]);return 2;}
}
if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(dump_ir?1:0)>1){
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);
+1
View File
@@ -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;
+3
View File
@@ -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;
+6 -1
View File
@@ -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;
}
+384
View File
@@ -0,0 +1,384 @@
#include "x86.h"
#include <string.h>
/* ------------------------------------------------------------------------- *
* 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);
}
+16
View File
@@ -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
+103
View File
@@ -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 <program.fe> [--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())
+1 -1
View File
@@ -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"