lower+backend: 배열, 경계검사, 구조체, 그리고 실행 테스트 스위트

인덱스는 부호 없는 비교와 트랩으로 펴진다. --no-checks 는 메시지가 아니라
비교와 분기 자체를 없앤다 -- 그게 그 플래그의 전부다.

버그 셋:

- 값으로 넘긴 구조체 파라미터는 주소로 도착하는데 lowering 이 그걸 몰라서
  포인터를 구조체로 읽었다. 변수마다 by_address 를 기록한다.
- 덩어리 반환의 숨은 결과 인자를 지역이 아니라 임시값으로 다뤘다.
- store 에 폭이 없어서 1바이트 bool 을 4바이트로 썼다. 옆 지역변수가
  뭉개졌고 logic 프로그램이 틀린 답을 냈다.

tests/exec.py 가 새 스위트다. run.py 는 컴파일러가 프로그램에 대해 뭐라고
하는지 보고, 이쪽은 프로그램이 실제로 무엇을 하는지 본다. 보고만 되고
방출되지 않는 경계검사는 저기서는 통과하고 여기서는 실패한다.

run.py 194/194, exec.py 6/6.
This commit is contained in:
2026-08-17 06:00:33 +09:00
parent e5093e690d
commit 43555b261c
11 changed files with 333 additions and 20 deletions
+3 -2
View File
@@ -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;
+4 -1
View File
@@ -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);
+136 -14
View File
@@ -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;
}
}
+2 -3
View File
@@ -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);
+12
View File
@@ -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;
}
+13
View File
@@ -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;
}
+10
View File
@@ -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;
}
+12
View File
@@ -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;
}
+14
View File
@@ -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;
}
+20
View File
@@ -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;
}
+107
View File
@@ -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())