빌드 전체가 한 모듈이라 파일 이름도 하나였다. std.list 안에서 터진 경계 검사가 프로그램의 파일 이름을 대고 있었으니, 줄 번호는 맞는데 파일이 틀려서 엉뚱한 줄을 가리켰다 -- 이름을 안 대는 것보다 나쁘다. 모듈이 파일 표를 들고 트랩은 그 인덱스를 든다. 생성기는 파일마다 FE_FILE_n 을 한 번씩 찍는다. before: index out of bounds at main.fe:2 after: index out of bounds at pick.fe:6 그리고 Parser.on 이 구조체 리터럴 안에서 다시 try 를 쓴다. 앞서 그것이 깨졌던 것은 try 때문이 아니라 Parser 가 1 바이트로 자리잡았기 때문이었다. 224/224, 31/31.
678 lines
25 KiB
C
678 lines
25 KiB
C
#include "x86.h"
|
|
#include <string.h>
|
|
#include <stdlib.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.
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/* Which register a temporary lives in, or none. Only ebx, esi and edi are
|
|
handed out: eax, ecx and edx are the scratch this emitter computes in, and
|
|
the three that are left survive a call without being saved. */
|
|
#define REG_NONE 0
|
|
#define REG_COUNT 3
|
|
static const char *const REGS[REG_COUNT] = { "ebx", "esi", "edi" };
|
|
|
|
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 */
|
|
/* 0 means the temporary lives in its stack slot. */
|
|
unsigned char *temp_reg;
|
|
} 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);
|
|
}
|
|
|
|
/* Which temporaries a value reads. Returns how many it wrote into `used`. */
|
|
static unsigned reads_of(const FeIrValue *v, unsigned *used)
|
|
{
|
|
unsigned n = 0;
|
|
unsigned i;
|
|
switch (v->op) {
|
|
case FE_IR_CONST: break;
|
|
case FE_IR_LOAD:
|
|
case FE_IR_ADDR:
|
|
if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index;
|
|
break;
|
|
case FE_IR_STORE:
|
|
if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index;
|
|
used[n++] = v->a;
|
|
break;
|
|
case FE_IR_COPY:
|
|
if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index;
|
|
if (v->place2.base == FE_PLACE_TEMP) used[n++] = v->place2.index;
|
|
break;
|
|
case FE_IR_CAST:
|
|
used[n++] = v->a;
|
|
break;
|
|
case FE_IR_CALL:
|
|
for (i = 0; i < v->arg_count && n < 18; ++i) used[n++] = v->args[i];
|
|
break;
|
|
default:
|
|
used[n++] = v->a;
|
|
used[n++] = v->b;
|
|
break;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* Give registers to the temporaries that can hold one.
|
|
|
|
A temporary that is defined in one block and read in another has to go
|
|
through memory: this walks one block at a time and knows nothing about the
|
|
others. Lowering does produce such temporaries -- a bounds check splits a
|
|
block between computing an index and using it -- so eligibility is decided
|
|
over the whole function first, and the scan inside a block only considers
|
|
what survived that. */
|
|
static void allocate_registers(Frame *fr, const FeIrFunc *f)
|
|
{
|
|
unsigned n = f->temp_count;
|
|
unsigned char *single; /* 1 while the temporary stays in one block */
|
|
unsigned *home; /* the block it was defined in */
|
|
unsigned *last; /* the last instruction in that block to read it */
|
|
const FeIrBlock *b;
|
|
const FeIrValue *v;
|
|
unsigned used[20];
|
|
unsigned i, k, at;
|
|
if (!n) { fr->temp_reg = 0; return; }
|
|
fr->temp_reg = (unsigned char *)calloc(n, 1);
|
|
single = (unsigned char *)calloc(n, 1);
|
|
home = (unsigned *)calloc(n, sizeof(unsigned));
|
|
last = (unsigned *)calloc(n, sizeof(unsigned));
|
|
if (!fr->temp_reg || !single || !home || !last) {
|
|
free(single); free(home); free(last);
|
|
return;
|
|
}
|
|
for (i = 0; i < n; ++i) { single[i] = 1; home[i] = 0xFFFFFFFFU; }
|
|
for (b = f->first; b; b = b->next) {
|
|
for (v = b->first; v; v = v->next) {
|
|
if (v->has_dest) {
|
|
if (home[v->dest] != 0xFFFFFFFFU) single[v->dest] = 0;
|
|
home[v->dest] = b->id;
|
|
}
|
|
k = reads_of(v, used);
|
|
for (i = 0; i < k; ++i)
|
|
if (used[i] < n && home[used[i]] != b->id) single[used[i]] = 0;
|
|
}
|
|
if (b->term == FE_IR_BR && b->cond < n && home[b->cond] != b->id)
|
|
single[b->cond] = 0;
|
|
if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n &&
|
|
home[b->ret_value] != b->id)
|
|
single[b->ret_value] = 0;
|
|
}
|
|
|
|
for (b = f->first; b; b = b->next) {
|
|
unsigned char busy[REG_COUNT];
|
|
unsigned owner[REG_COUNT];
|
|
for (i = 0; i < REG_COUNT; ++i) { busy[i] = 0; owner[i] = 0; }
|
|
/* When each temporary is last read in this block. */
|
|
at = 0;
|
|
for (v = b->first; v; v = v->next, ++at) {
|
|
k = reads_of(v, used);
|
|
for (i = 0; i < k; ++i)
|
|
if (used[i] < n && single[used[i]]) last[used[i]] = at;
|
|
}
|
|
if (b->term == FE_IR_BR && b->cond < n && single[b->cond])
|
|
last[b->cond] = at;
|
|
if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n &&
|
|
single[b->ret_value]) last[b->ret_value] = at;
|
|
|
|
at = 0;
|
|
for (v = b->first; v; v = v->next, ++at) {
|
|
/* Free whatever was read for the last time before this. */
|
|
for (i = 0; i < REG_COUNT; ++i)
|
|
if (busy[i] && last[owner[i]] < at) busy[i] = 0;
|
|
if (!v->has_dest || !single[v->dest]) continue;
|
|
/* A call clobbers the scratch registers but not these three, so a
|
|
result can still be kept in one across the call that made it. */
|
|
for (i = 0; i < REG_COUNT; ++i)
|
|
if (!busy[i]) {
|
|
busy[i] = 1;
|
|
owner[i] = v->dest;
|
|
fr->temp_reg[v->dest] = (unsigned char)(i + 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
free(single); free(home); free(last);
|
|
}
|
|
|
|
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_temp(const Frame *fr, unsigned t, const char *reg,
|
|
FILE *out);
|
|
|
|
static void load_place_base(const Frame *fr, const FeIrPlace *p, FILE *out)
|
|
{
|
|
if (p->base != FE_PLACE_TEMP) return;
|
|
/* Through load_temp, not straight from the slot: the pointer may be living
|
|
in a register, in which case the slot was never written. */
|
|
load_temp(fr, p->index, "edx", out);
|
|
}
|
|
|
|
static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out)
|
|
{
|
|
if (fr->temp_reg && fr->temp_reg[t]) {
|
|
const char *from = REGS[fr->temp_reg[t] - 1];
|
|
if (strcmp(from, reg) != 0)
|
|
fprintf(out, " mov %s, %s\n", reg, from);
|
|
return;
|
|
}
|
|
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)
|
|
{
|
|
if (fr->temp_reg && fr->temp_reg[t]) {
|
|
const char *to = REGS[fr->temp_reg[t] - 1];
|
|
if (strcmp(to, reg) != 0)
|
|
fprintf(out, " mov %s, %s\n", to, reg);
|
|
return;
|
|
}
|
|
fprintf(out, " mov [ebp%+ld], %s\n", temp_off(fr, t), reg);
|
|
}
|
|
|
|
/* The register a temporary lives in, or null when it lives in its slot. */
|
|
static const char *reg_home(const Frame *fr, unsigned t)
|
|
{
|
|
if (!fr->temp_reg || !fr->temp_reg[t]) return 0;
|
|
return REGS[fr->temp_reg[t] - 1];
|
|
}
|
|
|
|
/* Something an instruction can take as its right-hand operand: a register, or
|
|
the temporary's slot read in place. */
|
|
static void operand_of(const Frame *fr, unsigned t, char *buf)
|
|
{
|
|
const char *r = reg_home(fr, t);
|
|
if (r) strcpy(buf, r);
|
|
else sprintf(buf, "dword ptr [ebp%+ld]", temp_off(fr, t));
|
|
}
|
|
|
|
static const char *simple_op(FeIrOp op)
|
|
{
|
|
switch (op) {
|
|
case FE_IR_ADD: return "add ";
|
|
case FE_IR_SUB: return "sub ";
|
|
case FE_IR_AND: return "and ";
|
|
case FE_IR_OR: return "or ";
|
|
case FE_IR_XOR: return "xor ";
|
|
case FE_IR_MUL: return "imul";
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
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);
|
|
/* When the result has a register of its own and the operation is one that
|
|
can work on any register, the whole thing happens there: no trip through
|
|
the scratch register and no trip through memory.
|
|
|
|
Only the full-width operations qualify. esi and edi have no byte halves,
|
|
so a narrow operation still goes through eax, where they do. */
|
|
if (!is_cmp && v->has_dest && (t == FE_IR_I32 || t == FE_IR_PTR) &&
|
|
simple_op(v->op)) {
|
|
const char *d = reg_home(fr, v->dest);
|
|
const char *rb = reg_home(fr, v->b);
|
|
if (d && !(rb && strcmp(rb, d) == 0)) {
|
|
char right[64];
|
|
load_temp(fr, v->a, d, out);
|
|
operand_of(fr, v->b, right);
|
|
fprintf(out, " %s %s, %s\n", simple_op(v->op), d, right);
|
|
return;
|
|
}
|
|
}
|
|
/* A full-width comparison can read both sides where they already are; the
|
|
answer still has to come out of `al`, which is why it lands in eax when
|
|
the result has no register of its own. */
|
|
if (is_cmp && (t == FE_IR_I32 || t == FE_IR_PTR)) {
|
|
const char *left = reg_home(fr, v->a);
|
|
const char *d = reg_home(fr, v->dest);
|
|
char right[64];
|
|
if (!left) { load_temp(fr, v->a, "eax", out); left = "eax"; }
|
|
operand_of(fr, v->b, right);
|
|
fprintf(out, " cmp %s, %s\n", left, right);
|
|
fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned));
|
|
fprintf(out, " movzx %s, al\n", d ? d : "eax");
|
|
if (!d) store_temp(fr, v->dest, "eax", out);
|
|
return;
|
|
}
|
|
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: {
|
|
const char *d = reg_home(fr, v->dest);
|
|
fprintf(out, " mov %s, %ld\n", d ? d : "eax", v->imm);
|
|
if (!d) store_temp(fr, v->dest, "eax", out);
|
|
break;
|
|
}
|
|
case FE_IR_LOAD: {
|
|
const char *d = reg_home(fr, v->dest);
|
|
const char *into = d ? d : "eax";
|
|
load_place_base(fr, &v->place, out);
|
|
place_addr(fr, &v->place, addr);
|
|
if (v->type == FE_IR_I8)
|
|
fprintf(out, " movzx %s, byte ptr %s\n", into, addr);
|
|
else if (v->type == FE_IR_I16)
|
|
fprintf(out, " movzx %s, word ptr %s\n", into, addr);
|
|
else
|
|
fprintf(out, " mov %s, dword ptr %s\n", into, addr);
|
|
if (!d) store_temp(fr, v->dest, "eax", out);
|
|
break;
|
|
}
|
|
case FE_IR_STORE: {
|
|
const char *from = reg_home(fr, v->a);
|
|
load_place_base(fr, &v->place, out);
|
|
place_addr(fr, &v->place, addr);
|
|
/* A full-width value already in a register goes straight out; a narrow
|
|
one needs a byte or word half, which only eax has here. */
|
|
if (from && (v->type == FE_IR_I32 || v->type == FE_IR_PTR)) {
|
|
fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr,
|
|
from);
|
|
break;
|
|
}
|
|
load_temp(fr, v->a, "eax", out);
|
|
fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr,
|
|
reg_of(v->type, 0));
|
|
break;
|
|
}
|
|
case FE_IR_ADDR: {
|
|
const char *d = reg_home(fr, v->dest);
|
|
load_place_base(fr, &v->place, out);
|
|
place_addr(fr, &v->place, addr);
|
|
fprintf(out, " lea %s, %s\n", d ? d : "eax", addr);
|
|
if (!d) 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];
|
|
/* Both addresses are worked out in the scratch registers first, and
|
|
only then does the block copy take over esi and edi -- which may be
|
|
holding temporaries, so it hands them back. */
|
|
if (v->place2.base == FE_PLACE_TEMP) {
|
|
load_temp(fr, v->place2.index, "eax", out);
|
|
if (v->place2.offset)
|
|
fprintf(out, " add eax, %ld\n", v->place2.offset);
|
|
} else {
|
|
place_addr(fr, &v->place2, src);
|
|
fprintf(out, " lea eax, %s\n", src);
|
|
}
|
|
if (v->place.base == FE_PLACE_TEMP) {
|
|
load_temp(fr, v->place.index, "edx", out);
|
|
if (v->place.offset)
|
|
fprintf(out, " add edx, %ld\n", v->place.offset);
|
|
} else {
|
|
place_addr(fr, &v->place, dst);
|
|
fprintf(out, " lea edx, %s\n", dst);
|
|
}
|
|
fprintf(out, " push esi\n");
|
|
fprintf(out, " push edi\n");
|
|
fprintf(out, " mov esi, eax\n");
|
|
fprintf(out, " mov edi, edx\n");
|
|
fprintf(out, " mov ecx, %ld\n", v->imm);
|
|
fprintf(out, " cld\n");
|
|
fprintf(out, " rep movsb\n");
|
|
fprintf(out, " pop edi\n pop esi\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;
|
|
const FeIrBlock *b;
|
|
const FeIrValue *v;
|
|
unsigned i;
|
|
long arg = 8;
|
|
if (f->is_extern || !f->first) return;
|
|
/* One offset per local, however many there are. A fixed array here would
|
|
silently stop emitting a function that had too many. */
|
|
storage = (long *)malloc((size_t)(f->local_count ? f->local_count : 1) *
|
|
sizeof(long));
|
|
if (!storage) return;
|
|
frame_layout(&fr, f, storage);
|
|
allocate_registers(&fr, f);
|
|
|
|
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 ebx\n 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"
|
|
" pop ebx\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_FILE_%u\n",
|
|
b->trap_file);
|
|
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);
|
|
free(storage);
|
|
free(fr.temp_reg);
|
|
(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;
|
|
unsigned i;
|
|
|
|
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);
|
|
/* Anything called but not defined here lives somewhere else -- the runtime,
|
|
or a library. Lowering emits such calls directly (allocating, writing,
|
|
trapping), so the names are collected from the calls themselves rather
|
|
than from a list that would have to be kept in step. */
|
|
{
|
|
const char *seen[64];
|
|
unsigned count = 0;
|
|
const FeIrValue *v;
|
|
const FeIrFunc *g;
|
|
unsigned i;
|
|
for (f = m->funcs; f; f = f->next)
|
|
for (b = f->first; b; b = b->next)
|
|
for (v = b->first; v; v = v->next) {
|
|
if (v->op != FE_IR_CALL || !v->callee) continue;
|
|
for (g = m->funcs; g; g = g->next)
|
|
if (!strcmp(g->name, v->callee)) break;
|
|
if (g) continue;
|
|
for (i = 0; i < count; ++i)
|
|
if (!strcmp(seen[i], v->callee)) break;
|
|
if (i < count || count >= 64) continue;
|
|
seen[count++] = v->callee;
|
|
fprintf(out, "extern %s : near\n", v->callee);
|
|
}
|
|
}
|
|
if (any_trap) fputs("extern fe_trap : near\n", out);
|
|
|
|
fputs("\n_DATA segment dword public 'DATA'\n", out);
|
|
/* One name per file a trap can come from. A build is many units in
|
|
one module, and a trap that names the wrong file is worse than
|
|
one that names none. */
|
|
for (i = 0; i < m->file_count; ++i) {
|
|
fprintf(out, "public FE_FILE_%u\nFE_FILE_%u label byte\n", i, i);
|
|
emit_string(m->files[i], out);
|
|
}
|
|
for (g = m->globals; g; g = g->next) {
|
|
unsigned long i;
|
|
fprintf(out, "public %s\n%s label byte\n", g->name, g->name);
|
|
if (!g->init) {
|
|
fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL);
|
|
continue;
|
|
}
|
|
for (i = 0; i < g->size; ) {
|
|
unsigned r;
|
|
unsigned long j;
|
|
for (r = 0; r < g->reloc_count; ++r)
|
|
if (g->relocs[r].at == i) break;
|
|
if (r < g->reloc_count) {
|
|
/* A hole the linker fills with an address. */
|
|
fprintf(out, " dd offset %s\n",
|
|
g->relocs[r].symbol);
|
|
i += 4;
|
|
continue;
|
|
}
|
|
fputs(" db ", out);
|
|
j = 0;
|
|
while (i < g->size && j < 16) {
|
|
unsigned q;
|
|
for (q = 0; q < g->reloc_count; ++q)
|
|
if (g->relocs[q].at == i) break;
|
|
if (q < g->reloc_count) break;
|
|
fprintf(out, "%s%u", j ? "," : "", g->init[i]);
|
|
++i; ++j;
|
|
}
|
|
fputc('\n', out);
|
|
}
|
|
if (!g->size) fputs(" db 0\n", out);
|
|
}
|
|
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);
|
|
}
|