From 43555b261cd1cde697597918cd3bf38f06739eb6 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:00:33 +0900 Subject: [PATCH] =?UTF-8?q?lower+backend:=20=EB=B0=B0=EC=97=B4,=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=EA=B2=80=EC=82=AC,=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EC=B2=B4,=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=EC=9C=84=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 인덱스는 부호 없는 비교와 트랩으로 펴진다. --no-checks 는 메시지가 아니라 비교와 분기 자체를 없앤다 -- 그게 그 플래그의 전부다. 버그 셋: - 값으로 넘긴 구조체 파라미터는 주소로 도착하는데 lowering 이 그걸 몰라서 포인터를 구조체로 읽었다. 변수마다 by_address 를 기록한다. - 덩어리 반환의 숨은 결과 인자를 지역이 아니라 임시값으로 다뤘다. - store 에 폭이 없어서 1바이트 bool 을 4바이트로 썼다. 옆 지역변수가 뭉개졌고 logic 프로그램이 틀린 답을 냈다. tests/exec.py 가 새 스위트다. run.py 는 컴파일러가 프로그램에 대해 뭐라고 하는지 보고, 이쪽은 프로그램이 실제로 무엇을 하는지 본다. 보고만 되고 방출되지 않는 경계검사는 저기서는 통과하고 여기서는 실패한다. run.py 194/194, exec.py 6/6. --- fec/src/ir.c | 5 +- fec/src/ir.h | 5 +- fec/src/lower.c | 150 +++++++++++++++++++++++++++++++++---- fec/src/x86.c | 5 +- fec/tests/exec/arith.fe | 12 +++ fec/tests/exec/array.fe | 13 ++++ fec/tests/exec/bounds.fe | 10 +++ fec/tests/exec/logic.fe | 12 +++ fec/tests/exec/loopcall.fe | 14 ++++ fec/tests/exec/structs.fe | 20 +++++ tests/exec.py | 107 ++++++++++++++++++++++++++ 11 files changed, 333 insertions(+), 20 deletions(-) create mode 100644 fec/tests/exec/arith.fe create mode 100644 fec/tests/exec/array.fe create mode 100644 fec/tests/exec/bounds.fe create mode 100644 fec/tests/exec/logic.fe create mode 100644 fec/tests/exec/loopcall.fe create mode 100644 fec/tests/exec/structs.fe create mode 100644 tests/exec.py diff --git a/fec/src/ir.c b/fec/src/ir.c index b5ebb20..959a75f 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -144,9 +144,10 @@ unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p) return result(m, b, v); } -void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned value) +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned value, + FeIrType t) { - FeIrValue *v = emit(m, b, FE_IR_STORE, FE_IR_VOID); + FeIrValue *v = emit(m, b, FE_IR_STORE, t); if (!v) return; v->place = p; v->a = value; diff --git a/fec/src/ir.h b/fec/src/ir.h index 38b5acb..edee432 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -160,7 +160,10 @@ FeIrPlace fe_ir_at_temp(unsigned temp, long offset); /* Instructions. Each returns the destination temporary where there is one. */ unsigned fe_ir_const(FeIrModule *m, FeIrBlock *b, FeIrType t, long v); unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p); -void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned v); +/* `t` is how wide the write is. Without it a one-byte value would be stored + four bytes wide and take its neighbours with it. */ +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned v, + FeIrType t); unsigned fe_ir_addr(FeIrModule *m, FeIrBlock *b, FeIrPlace p); unsigned fe_ir_binary(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t, unsigned a, unsigned c, int is_unsigned); diff --git a/fec/src/lower.c b/fec/src/lower.c index 23926ea..4bc3d6a 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -17,6 +17,9 @@ typedef struct LowerVar { const char *cname; unsigned local; + /* An aggregate parameter arrives as an address, so the slot holds a + pointer and the value is one dereference away. */ + int by_address; } LowerVar; typedef struct Lower { @@ -45,6 +48,8 @@ typedef struct Slot { static Slot lower_expr(Lower *L, FeNode *n); static void lower_stmt(Lower *L, FeNode *n); +static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size); static void fail(Lower *L, const char *why, FeNode *n) { @@ -163,20 +168,19 @@ static unsigned declare_var(Lower *L, const char *cname, const FeType *t, if (L->var_count < LOWER_MAX_LOCALS) { L->vars[L->var_count].cname = cname; L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = 0; ++L->var_count; } return local; } -static int find_var(Lower *L, const char *cname, unsigned *out) +static LowerVar *find_var(Lower *L, const char *cname) { unsigned i; if (!cname) return 0; for (i = L->var_count; i > 0; --i) - if (L->vars[i - 1].cname && strcmp(L->vars[i - 1].cname, cname) == 0) { - *out = L->vars[i - 1].local; - return 1; - } + if (L->vars[i - 1].cname && strcmp(L->vars[i - 1].cname, cname) == 0) + return &L->vars[i - 1]; return 0; } @@ -187,6 +191,50 @@ static FeIrBlock *new_block(Lower *L) return fe_ir_block(L->m, L->fn); } +/* A check that must hold. `ok` is a condition; when it is false the program + stops where it is. `--no-checks` removes the comparison and the branch, not + just the message, which is the whole point of the flag. */ +static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) +{ + FeIrBlock *bad = new_block(L); + FeIrBlock *cont = new_block(L); + fe_ir_br(L->b, ok, cont->id, bad->id); + L->b = bad; + fe_ir_trap(L->b, reason, line); + L->b = cont; +} + +/* Somewhere to build an aggregate that has no home of its own yet. */ +static unsigned scratch(Lower *L, const FeType *t, const char *why) +{ + return fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), ir_align(t), why); +} + +/* A slice is a pointer and a length, in that order. Both the compiler and the + runtime read it this way, so the offsets live here and nowhere else. */ +#define SLICE_PTR_OFFSET 0L +#define SLICE_LEN_OFFSET 4L + +/* The number of elements an indexable place holds, and where the first element + is. An array is its own storage; a slice points at someone else's. */ +static void indexable_parts(Lower *L, Slot base, const FeType *t, + unsigned *data, unsigned *length, FeNode *n) +{ + if (t && t->kind == FE_TYPE_ARRAY) { + *data = as_address(L, base, n); + *length = fe_ir_const(L->m, L->b, FE_IR_I32, (long)t->length); + return; + } + if (!base.is_place) { fail(L, "a slice with no place", n); *data = 0; *length = 0; return; } + *data = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_temp(as_address(L, base, n), SLICE_PTR_OFFSET)); + { + FeIrPlace lp = base.place; + lp.offset += SLICE_LEN_OFFSET; + *length = fe_ir_load(L->m, L->b, FE_IR_I32, lp); + } +} + /* ---------------------------------------------------------- expressions --- */ static FeIrOp binary_op(const char *op, int *is_cmp) @@ -268,12 +316,12 @@ static Slot lower_logical(Lower *L, FeNode *n, int is_and) unsigned right; L->b = entry; left = as_value(L, lower_expr(L, n->a), n->a); - fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), left); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), left, FE_IR_I8); if (is_and) fe_ir_br(L->b, left, rhs->id, join->id); else fe_ir_br(L->b, left, join->id, rhs->id); L->b = rhs; right = as_value(L, lower_expr(L, n->b), n->b); - fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), right); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), right, FE_IR_I8); fe_ir_jmp(L->b, join->id); L->b = join; return slot_place(fe_ir_at_local(result, 0), FE_IR_I8, 1); @@ -329,9 +377,15 @@ static Slot lower_expr(Lower *L, FeNode *n) literal_value(n)), it == FE_IR_VOID ? FE_IR_I32 : it); case FE_N_IDENT: { - unsigned local; - if (find_var(L, n->cname, &local)) - return slot_place(fe_ir_at_local(local, 0), it, ir_size(t)); + LowerVar *var = find_var(L, n->cname); + if (var) { + if (var->by_address) { + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(var->local, 0)); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + return slot_place(fe_ir_at_local(var->local, 0), it, ir_size(t)); + } if (n->cname) return slot_place(fe_ir_at_global(n->cname, 0), it, ir_size(t)); fail(L, "an unresolved name", n); @@ -380,6 +434,19 @@ static Slot lower_expr(Lower *L, FeNode *n) unsigned p = as_value(L, lower_expr(L, n->a), n->a); return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); } + /* `.n` is how many elements there are, which an array knows at + compile time and a slice carries beside its pointer. */ + if (n->b && n->b->text && !strcmp(n->b->text, "n")) { + FeType *bt = n->a ? n->a->sem_type : 0; + Slot base; + if (bt && bt->kind == FE_TYPE_ARRAY) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)bt->length), FE_IR_I32); + base = lower_expr(L, n->a); + if (!base.is_place) { fail(L, "a length of a temporary", n); return slot_void(); } + base.place.offset += SLICE_LEN_OFFSET; + return slot_place(base.place, FE_IR_I32, 4); + } /* A field is a constant offset from the base. */ { FeType *base = n->a ? n->a->sem_type : 0; @@ -400,6 +467,58 @@ static Slot lower_expr(Lower *L, FeNode *n) b.place.offset += (long)field->offset; return slot_place(b.place, it, ir_size(t)); } + case FE_N_INDEX: { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base; + unsigned data; + unsigned length; + unsigned index; + unsigned scale; + unsigned offset; + unsigned addr; + if (n->c || !n->b) { fail(L, "a slice expression", n); return slot_void(); } + base = lower_expr(L, n->a); + indexable_parts(L, base, bt, &data, &length, n); + index = as_value(L, lower_expr(L, n->b), n->b); + if (!L->c->no_checks) { + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, + index, length, 1); + guard(L, ok, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + offset = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, index, scale, 1); + addr = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, offset, 1); + return slot_place(fe_ir_at_temp(addr, 0), ir_type(elem), ir_size(elem)); + } + case FE_N_ARRAY_INIT: { + unsigned local = scratch(L, t, "array"); + FeType *elem = t ? t->elem : 0; + unsigned long step = ir_size(elem); + long at = 0; + FeNode *x; + for (x = n->children; x; x = x->next) { + Slot v = lower_expr(L, x); + store_into(L, fe_ir_at_local(local, at), v, x, step); + at += (long)step; + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + case FE_N_STRUCT_INIT: { + unsigned local = scratch(L, t, "struct"); + FeNode *f; + for (f = n->children; f; f = f->next) { + FeFieldType *field; + Slot v; + if (f->kind != FE_N_FIELD) continue; + field = fe_type_field(t, f->text); + if (!field) { fail(L, "an unresolved field", f); return slot_void(); } + v = lower_expr(L, f->a); + store_into(L, fe_ir_at_local(local, (long)field->offset), v, f, + ir_size(field->type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } case FE_N_CALL: return lower_call(L, n); case FE_N_EXPR: @@ -420,7 +539,7 @@ static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, fe_ir_copy(L->m, L->b, dst, value.place, size); return; } - fe_ir_store(L->m, L->b, dst, as_value(L, value, n)); + fe_ir_store(L->m, L->b, dst, as_value(L, value, n), value.type); } static void lower_return(Lower *L, FeNode *n) @@ -429,8 +548,9 @@ static void lower_return(Lower *L, FeNode *n) if (!n->a) { fe_ir_ret(L->b, 0, 0); return; } v = lower_expr(L, n->a); if (L->fn->returns_by_address) { - store_into(L, fe_ir_at_temp(L->ret_local, 0), v, n, - ir_size(L->ret_type)); + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + store_into(L, fe_ir_at_temp(dst, 0), v, n, ir_size(L->ret_type)); fe_ir_ret(L->b, 0, 0); return; } @@ -550,13 +670,15 @@ static void lower_fn(Lower *L, FeNode *fn) for (p = fn->a ? fn->a->children : 0; p; p = p->next) { FeType *pt = fe_type_from_ast(&L->c->types, p->a); /* An aggregate parameter arrives as an address. */ - unsigned local = ir_type(pt) == FE_IR_MEM + int by_address = ir_type(pt) == FE_IR_MEM; + unsigned local = by_address ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), p->text); if (L->var_count < LOWER_MAX_LOCALS) { L->vars[L->var_count].cname = p->cname; L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = by_address; ++L->var_count; } } diff --git a/fec/src/x86.c b/fec/src/x86.c index 11c6ef0..05348a1 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -204,9 +204,8 @@ static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) 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)); + fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, + reg_of(v->type, 0)); break; case FE_IR_ADDR: load_place_base(fr, &v->place, out); diff --git a/fec/tests/exec/arith.fe b/fec/tests/exec/arith.fe new file mode 100644 index 0000000..e9cb2ff --- /dev/null +++ b/fec/tests/exec/arith.fe @@ -0,0 +1,12 @@ +// EXIT:10 +unit arith; + +fn main() -> i32 { + let a: i32 = 7; + let b: i32 = 6; + var r: i32 = a * b; + r = r - 2; + r = r / 4; + if r == 10 { return r; } + return 99; +} diff --git a/fec/tests/exec/array.fe b/fec/tests/exec/array.fe new file mode 100644 index 0000000..cf1e7ce --- /dev/null +++ b/fec/tests/exec/array.fe @@ -0,0 +1,13 @@ +// EXIT:100 +unit array; + +fn main() -> i32 { + let a: [4]i32 = [10, 20, 30, 40]; + var sum: i32 = 0; + var i: i32 = 0; + while i < 4 { + sum = sum + a[i]; + i = i + 1; + } + return sum; +} diff --git a/fec/tests/exec/bounds.fe b/fec/tests/exec/bounds.fe new file mode 100644 index 0000000..3772539 --- /dev/null +++ b/fec/tests/exec/bounds.fe @@ -0,0 +1,10 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// NOCHECKS:0 +unit bounds; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let x: i32 = a[2]; + return x - x; +} diff --git a/fec/tests/exec/logic.fe b/fec/tests/exec/logic.fe new file mode 100644 index 0000000..dc65c94 --- /dev/null +++ b/fec/tests/exec/logic.fe @@ -0,0 +1,12 @@ +// EXIT:1 +unit logic; + +fn side(v: i32) -> bool { return v > 0; } + +fn main() -> i32 { + let a: bool = true and side(1); + let b: bool = false or side(2); + let c: bool = not side(0); + if a and b and c { return 1; } + return 0; +} diff --git a/fec/tests/exec/loopcall.fe b/fec/tests/exec/loopcall.fe new file mode 100644 index 0000000..f3b3bb1 --- /dev/null +++ b/fec/tests/exec/loopcall.fe @@ -0,0 +1,14 @@ +// EXIT:55 +unit loopcall; + +fn add(a: i32, b: i32) -> i32 { return a + b; } + +fn main() -> i32 { + var total: i32 = 0; + var i: i32 = 0; + while i < 10 { + total = total + add(i, 1); + i = i + 1; + } + return total; +} diff --git a/fec/tests/exec/structs.fe b/fec/tests/exec/structs.fe new file mode 100644 index 0000000..826966b --- /dev/null +++ b/fec/tests/exec/structs.fe @@ -0,0 +1,20 @@ +// EXIT:97 +unit structs; + +struct Point { x: i32, y: i32, } + +fn make(a: i32, b: i32) -> Point { return Point{ x: a, y: b }; } +fn swap(p: Point) -> Point { return Point{ x: p.y, y: p.x }; } + +fn main() -> i32 { + let p: Point = make(3, 8); + let q: Point = swap(p); + let grid: [3]Point = [make(1,1), make(2,2), make(3,3)]; + var s: i32 = 0; + var i: i32 = 0; + while i < 3 { + s = s + grid[i].x * grid[i].y; + i = i + 1; + } + return q.x * 10 + q.y + s; +} diff --git a/tests/exec.py b/tests/exec.py new file mode 100644 index 0000000..92d0ea6 --- /dev/null +++ b/tests/exec.py @@ -0,0 +1,107 @@ +"""Compile the programs under `fec/tests/exec/`, run them, and check what they do. + +A program says what it should do in its first lines: + + // EXIT:55 the process must exit with this code + // OUTPUT:hello this text must appear in what it wrote + // NOCHECKS:0 build it a second time with --no-checks and expect + this exit code instead + +The point of this suite is different from `run.py`. That one checks what the +compiler says about a program; this one checks what the program does. A bounds +check that is reported but never emitted passes there and fails here. +""" +from __future__ import annotations + +import argparse +import re +import shutil +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build as builder # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +PROGRAMS = ROOT / "fec" / "tests" / "exec" + + +def expectations(path: Path) -> dict: + want = {} + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line.startswith("//"): + break + m = re.match(r"//\s*(EXIT|OUTPUT|NOCHECKS):(.*)", line) + if m: + key, value = m.group(1), m.group(2).strip() + want[key] = int(value) if key in ("EXIT", "NOCHECKS") else value + return want + + +def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]: + want = expectations(path) + if "EXIT" not in want: + return False, "no // EXIT: marker" + + exe, log = builder.build(fec, path, out_dir) + if not exe: + detail = "\n".join(f" {n} (exit {c}): {t.strip()}" + for n, c, t in log if c != 0 or t.strip()) + return False, "did not build\n" + detail + code, text = builder.run(exe) + if code != want["EXIT"]: + return False, f"exited {code}, expected {want['EXIT']}\n {text.strip()}" + if "OUTPUT" in want and want["OUTPUT"] not in text: + return False, f"output has no {want['OUTPUT']!r}\n {text.strip()}" + + if "NOCHECKS" in want: + exe2, log2 = builder.build(fec, path, out_dir / "nochecks", + no_checks=True) + if not exe2: + return False, "did not build with --no-checks" + code2, _ = builder.run(exe2) + if code2 != want["NOCHECKS"]: + return False, (f"--no-checks exited {code2}, expected " + f"{want['NOCHECKS']}") + return True, "" + + +def main() -> int: + ap = argparse.ArgumentParser(description="run the compiled programs") + ap.add_argument("-k", dest="select") + ap.add_argument("-v", dest="verbose", action="store_true") + args = ap.parse_args() + + fec = ROOT / ".build" / "fec.exe" + if not fec.is_file(): + print("build the front end first: uv run python tests/run.py") + return 2 + out_dir = ROOT / ".build" / "exec" + if out_dir.exists(): + shutil.rmtree(out_dir, ignore_errors=True) + + cases = sorted(PROGRAMS.rglob("*.fe")) + if args.select: + cases = [p for p in cases if args.select in p.as_posix()] + if not cases: + print("no programs found") + return 1 + + failed = [] + for path in cases: + ok, why = check_one(fec, path, out_dir / path.stem) + rel = path.relative_to(PROGRAMS).as_posix() + if ok: + if args.verbose: + print(f" ok {rel}") + else: + failed.append((rel, why)) + for rel, why in failed: + print(f"FAIL {rel}: {why}") + print(f"\n{len(cases) - len(failed)}/{len(cases)} programs behaved") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main())