Files
doslang-mirror/fec/src/x86.c
T
coolguy 63ad48cd8a 트랩은 검사가 쓰인 파일을 댄다
빌드 전체가 한 모듈이라 파일 이름도 하나였다. 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.
2026-08-17 12:56:34 +09:00

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);
}